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 * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window.
65 public class TTerminalWindow
extends TScrollableWindow
66 implements DisplayListener
{
71 private static final ResourceBundle i18n
= ResourceBundle
.getBundle(TTerminalWindow
.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;
95 * If true, close the window when the shell exits.
97 private boolean closeOnExit
= false;
100 * Double-height font.
102 private GlyphMaker doubleFont
;
105 * Last text width value.
107 private int lastTextWidth
= -1;
110 * Last text height value.
112 private int lastTextHeight
= -1;
115 * The blink state, used only by ECMA48 backend and when double-width
116 * chars must be drawn.
118 private boolean blinkState
= true;
121 * Timer flag, used only by ECMA48 backend and when double-width chars
124 private boolean haveTimer
= false;
127 * The last seen scrollback lines.
129 private List
<DisplayLine
> scrollback
;
132 * The last seen display lines.
134 private List
<DisplayLine
> display
;
137 * If true, the display has changed and needs updating.
139 private volatile boolean dirty
= true;
142 * Time that the display was last updated.
144 private long lastUpdateTime
= 0;
146 // ------------------------------------------------------------------------
147 // Constructors -----------------------------------------------------------
148 // ------------------------------------------------------------------------
151 * Public constructor spawns a custom command line.
153 * @param application TApplication that manages this window
154 * @param x column relative to parent
155 * @param y row relative to parent
156 * @param commandLine the command line to execute
158 public TTerminalWindow(final TApplication application
, final int x
,
159 final int y
, final String commandLine
) {
161 this(application
, x
, y
, RESIZABLE
, commandLine
.split("\\s+"),
162 System
.getProperty("jexer.TTerminal.closeOnExit",
163 "false").equals("true"));
167 * Public constructor spawns a custom command line.
169 * @param application TApplication that manages this window
170 * @param x column relative to parent
171 * @param y row relative to parent
172 * @param commandLine the command line to execute
173 * @param closeOnExit if true, close the window when the command exits
175 public TTerminalWindow(final TApplication application
, final int x
,
176 final int y
, final String commandLine
, final boolean closeOnExit
) {
178 this(application
, x
, y
, RESIZABLE
, commandLine
.split("\\s+"),
183 * Public constructor spawns a custom command line.
185 * @param application TApplication that manages this window
186 * @param x column relative to parent
187 * @param y row relative to parent
188 * @param flags mask of CENTERED, MODAL, or RESIZABLE
189 * @param command the command line to execute
191 public TTerminalWindow(final TApplication application
, final int x
,
192 final int y
, final int flags
, final String
[] command
) {
194 this(application
, x
, y
, flags
, command
,
195 System
.getProperty("jexer.TTerminal.closeOnExit",
196 "false").equals("true"));
200 * Public constructor spawns a custom command line.
202 * @param application TApplication that manages this window
203 * @param x column relative to parent
204 * @param y row relative to parent
205 * @param flags mask of CENTERED, MODAL, or RESIZABLE
206 * @param command the command line to execute
207 * @param closeOnExit if true, close the window when the command exits
209 public TTerminalWindow(final TApplication application
, final int x
,
210 final int y
, final int flags
, final String
[] command
,
211 final boolean closeOnExit
) {
213 super(application
, i18n
.getString("windowTitle"), x
, y
,
214 80 + 2, 24 + 2, flags
);
216 this.closeOnExit
= closeOnExit
;
218 String
[] fullCommand
;
220 // Spawn a shell and pass its I/O to the other constructor.
221 if ((System
.getProperty("jexer.TTerminal.ptypipe") != null)
222 && (System
.getProperty("jexer.TTerminal.ptypipe").
226 fullCommand
= new String
[command
.length
+ 1];
227 fullCommand
[0] = "ptypipe";
228 System
.arraycopy(command
, 0, fullCommand
, 1, command
.length
);
229 } else if (System
.getProperty("os.name").startsWith("Windows")) {
230 fullCommand
= new String
[3];
231 fullCommand
[0] = "cmd";
232 fullCommand
[1] = "/c";
233 fullCommand
[2] = stringArrayToString(command
);
234 } else if (System
.getProperty("os.name").startsWith("Mac")) {
235 fullCommand
= new String
[6];
236 fullCommand
[0] = "script";
237 fullCommand
[1] = "-q";
238 fullCommand
[2] = "-F";
239 fullCommand
[3] = "/dev/null";
240 fullCommand
[4] = "-c";
241 fullCommand
[5] = stringArrayToString(command
);
243 // Default: behave like Linux
244 fullCommand
= new String
[5];
245 fullCommand
[0] = "script";
246 fullCommand
[1] = "-fqe";
247 fullCommand
[2] = "/dev/null";
248 fullCommand
[3] = "-c";
249 fullCommand
[4] = stringArrayToString(command
);
251 spawnShell(fullCommand
);
255 * Public constructor spawns a shell.
257 * @param application TApplication that manages this window
258 * @param x column relative to parent
259 * @param y row relative to parent
260 * @param flags mask of CENTERED, MODAL, or RESIZABLE
262 public TTerminalWindow(final TApplication application
, final int x
,
263 final int y
, final int flags
) {
265 this(application
, x
, y
, flags
,
266 System
.getProperty("jexer.TTerminal.closeOnExit",
267 "false").equals("true"));
272 * Public constructor spawns a shell.
274 * @param application TApplication that manages this window
275 * @param x column relative to parent
276 * @param y row relative to parent
277 * @param flags mask of CENTERED, MODAL, or RESIZABLE
278 * @param closeOnExit if true, close the window when the shell exits
280 public TTerminalWindow(final TApplication application
, final int x
,
281 final int y
, final int flags
, final boolean closeOnExit
) {
283 super(application
, i18n
.getString("windowTitle"), x
, y
,
284 80 + 2, 24 + 2, flags
);
286 this.closeOnExit
= closeOnExit
;
288 String cmdShellWindows
= "cmd.exe";
290 // You cannot run a login shell in a bare Process interactively, due
291 // to libc's behavior of buffering when stdin/stdout aren't a tty.
292 // Use 'script' instead to run a shell in a pty. And because BSD and
293 // GNU differ on the '-f' vs '-F' flags, we need two different
295 String cmdShellGNU
= "script -fqe /dev/null";
296 String cmdShellBSD
= "script -q -F /dev/null";
298 // ptypipe is another solution that permits dynamic window resizing.
299 String cmdShellPtypipe
= "ptypipe /bin/bash --login";
301 // Spawn a shell and pass its I/O to the other constructor.
302 if ((System
.getProperty("jexer.TTerminal.ptypipe") != null)
303 && (System
.getProperty("jexer.TTerminal.ptypipe").
307 spawnShell(cmdShellPtypipe
.split("\\s+"));
308 } else if (System
.getProperty("os.name").startsWith("Windows")) {
309 spawnShell(cmdShellWindows
.split("\\s+"));
310 } else if (System
.getProperty("os.name").startsWith("Mac")) {
311 spawnShell(cmdShellBSD
.split("\\s+"));
312 } else if (System
.getProperty("os.name").startsWith("Linux")) {
313 spawnShell(cmdShellGNU
.split("\\s+"));
315 // When all else fails, assume GNU.
316 spawnShell(cmdShellGNU
.split("\\s+"));
320 // ------------------------------------------------------------------------
321 // TScrollableWindow ------------------------------------------------------
322 // ------------------------------------------------------------------------
325 * Draw the display buffer.
329 int width
= getDisplayWidth();
330 boolean syncEmulator
= false;
331 if ((System
.currentTimeMillis() - lastUpdateTime
>= 25)
334 // Too much time has passed, draw it all.
336 } else if (emulator
.isReading() && (dirty
== false)) {
337 // Wait until the emulator has brought more data in.
338 syncEmulator
= false;
339 } else if (!emulator
.isReading() && (dirty
== true)) {
340 // The emulator won't receive more data, update the display.
344 if ((syncEmulator
== true)
345 || (scrollback
== null)
348 // We want to minimize the amount of time we have the emulator
349 // locked. Grab a copy of its display.
350 synchronized (emulator
) {
351 // Update the scroll bars
354 if ((scrollback
== null) || emulator
.isReading()) {
355 scrollback
= copyBuffer(emulator
.getScrollbackBuffer());
356 display
= copyBuffer(emulator
.getDisplayBuffer());
358 width
= emulator
.getWidth();
363 // Draw the box using my superclass
366 // Put together the visible rows
367 int visibleHeight
= getHeight() - 2;
368 int visibleBottom
= scrollback
.size() + display
.size()
369 + getVerticalValue();
370 assert (visibleBottom
>= 0);
372 List
<DisplayLine
> preceedingBlankLines
= new ArrayList
<DisplayLine
>();
373 int visibleTop
= visibleBottom
- visibleHeight
;
374 if (visibleTop
< 0) {
375 for (int i
= visibleTop
; i
< 0; i
++) {
376 preceedingBlankLines
.add(emulator
.getBlankDisplayLine());
380 assert (visibleTop
>= 0);
382 List
<DisplayLine
> displayLines
= new ArrayList
<DisplayLine
>();
383 displayLines
.addAll(scrollback
);
384 displayLines
.addAll(display
);
386 List
<DisplayLine
> visibleLines
= new ArrayList
<DisplayLine
>();
387 visibleLines
.addAll(preceedingBlankLines
);
388 visibleLines
.addAll(displayLines
.subList(visibleTop
,
391 visibleHeight
-= visibleLines
.size();
392 assert (visibleHeight
>= 0);
394 // Now draw the emulator screen
396 for (DisplayLine line
: visibleLines
) {
397 int widthMax
= width
;
398 if (line
.isDoubleWidth()) {
401 if (widthMax
> getWidth() - 2) {
402 widthMax
= getWidth() - 2;
404 for (int i
= 0; i
< widthMax
; i
++) {
405 Cell ch
= line
.charAt(i
);
408 putCharXY(i
+ 1, row
, ch
);
412 Cell newCell
= new Cell(ch
);
413 boolean reverse
= line
.isReverseColor() ^ ch
.isReverse();
414 newCell
.setReverse(false);
416 if (ch
.getForeColorRGB() < 0) {
417 newCell
.setBackColor(ch
.getForeColor());
418 newCell
.setBackColorRGB(-1);
420 newCell
.setBackColorRGB(ch
.getForeColorRGB());
422 if (ch
.getBackColorRGB() < 0) {
423 newCell
.setForeColor(ch
.getBackColor());
424 newCell
.setForeColorRGB(-1);
426 newCell
.setForeColorRGB(ch
.getBackColorRGB());
429 if (line
.isDoubleWidth()) {
430 putDoubleWidthCharXY(line
, (i
* 2) + 1, row
, newCell
);
432 putCharXY(i
+ 1, row
, newCell
);
436 if (row
== getHeight() - 1) {
437 // Don't overwrite the box edge
441 CellAttributes background
= new CellAttributes();
442 // Fill in the blank lines on bottom
443 for (int i
= 0; i
< visibleHeight
; i
++) {
444 hLineXY(1, i
+ row
, getWidth() - 2, ' ', background
);
450 * Handle window close.
453 public void onClose() {
456 terminateShellChildProcess();
463 * Handle window/screen resize events.
465 * @param resize resize event
468 public void onResize(final TResizeEvent resize
) {
470 // Synchronize against the emulator so we don't stomp on its reader
472 synchronized (emulator
) {
474 if (resize
.getType() == TResizeEvent
.Type
.WIDGET
) {
475 // Resize the scroll bars
479 // Get out of scrollback
483 emulator
.setWidth(getWidth() - 2);
484 emulator
.setHeight(getHeight() - 2);
486 emulator
.writeRemote("\033[8;" + (getHeight() - 2) + ";" +
487 (getWidth() - 2) + "t");
492 } // synchronized (emulator)
496 * Resize scrollbars for a new width/height.
499 public void reflowData() {
501 // Synchronize against the emulator so we don't stomp on its reader
503 synchronized (emulator
) {
505 // Pull cursor information
508 // Vertical scrollbar
509 setTopValue(getHeight() - 2
510 - (emulator
.getScrollbackBuffer().size()
511 + emulator
.getDisplayBuffer().size()));
512 setVerticalBigChange(getHeight() - 2);
514 } // synchronized (emulator)
520 * @param keypress keystroke event
523 public void onKeypress(final TKeypressEvent keypress
) {
525 // Scrollback up/down
526 if (keypress
.equals(kbShiftPgUp
)
527 || keypress
.equals(kbCtrlPgUp
)
528 || keypress
.equals(kbAltPgUp
)
530 bigVerticalDecrement();
533 if (keypress
.equals(kbShiftPgDn
)
534 || keypress
.equals(kbCtrlPgDn
)
535 || keypress
.equals(kbAltPgDn
)
537 bigVerticalIncrement();
541 if (emulator
.isReading()) {
542 // Get out of scrollback
544 emulator
.addUserEvent(keypress
);
546 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
547 // this is kBEnter then also send kbCtrlJ.
548 if (System
.getProperty("os.name").startsWith("Windows")) {
549 if (keypress
.equals(kbEnter
)) {
550 emulator
.addUserEvent(new TKeypressEvent(kbCtrlJ
));
558 // Process is closed, honor "normal" TUI keystrokes
559 super.onKeypress(keypress
);
563 * Handle mouse press events.
565 * @param mouse mouse button press event
568 public void onMouseDown(final TMouseEvent mouse
) {
569 if (inWindowMove
|| inWindowResize
) {
570 // TWindow needs to deal with this.
571 super.onMouseDown(mouse
);
575 // If the emulator is tracking mouse buttons, it needs to see wheel
577 if (emulator
.getMouseProtocol() == ECMA48
.MouseProtocol
.OFF
) {
578 if (mouse
.isMouseWheelUp()) {
582 if (mouse
.isMouseWheelDown()) {
587 if (mouseOnEmulator(mouse
)) {
588 mouse
.setX(mouse
.getX() - 1);
589 mouse
.setY(mouse
.getY() - 1);
590 emulator
.addUserEvent(mouse
);
595 // Emulator didn't consume it, pass it on
596 super.onMouseDown(mouse
);
600 * Handle mouse release events.
602 * @param mouse mouse button release event
605 public void onMouseUp(final TMouseEvent mouse
) {
606 if (inWindowMove
|| inWindowResize
) {
607 // TWindow needs to deal with this.
608 super.onMouseUp(mouse
);
612 if (mouseOnEmulator(mouse
)) {
613 mouse
.setX(mouse
.getX() - 1);
614 mouse
.setY(mouse
.getY() - 1);
615 emulator
.addUserEvent(mouse
);
620 // Emulator didn't consume it, pass it on
621 super.onMouseUp(mouse
);
625 * Handle mouse motion events.
627 * @param mouse mouse motion event
630 public void onMouseMotion(final TMouseEvent mouse
) {
631 if (inWindowMove
|| inWindowResize
) {
632 // TWindow needs to deal with this.
633 super.onMouseMotion(mouse
);
637 if (mouseOnEmulator(mouse
)) {
638 mouse
.setX(mouse
.getX() - 1);
639 mouse
.setY(mouse
.getY() - 1);
640 emulator
.addUserEvent(mouse
);
645 // Emulator didn't consume it, pass it on
646 super.onMouseMotion(mouse
);
649 // ------------------------------------------------------------------------
650 // TTerminalWindow --------------------------------------------------------
651 // ------------------------------------------------------------------------
654 * Claim the keystrokes the emulator will need.
656 private void addShortcutKeys() {
657 addShortcutKeypress(kbCtrlA
);
658 addShortcutKeypress(kbCtrlB
);
659 addShortcutKeypress(kbCtrlC
);
660 addShortcutKeypress(kbCtrlD
);
661 addShortcutKeypress(kbCtrlE
);
662 addShortcutKeypress(kbCtrlF
);
663 addShortcutKeypress(kbCtrlG
);
664 addShortcutKeypress(kbCtrlH
);
665 addShortcutKeypress(kbCtrlU
);
666 addShortcutKeypress(kbCtrlJ
);
667 addShortcutKeypress(kbCtrlK
);
668 addShortcutKeypress(kbCtrlL
);
669 addShortcutKeypress(kbCtrlM
);
670 addShortcutKeypress(kbCtrlN
);
671 addShortcutKeypress(kbCtrlO
);
672 addShortcutKeypress(kbCtrlP
);
673 addShortcutKeypress(kbCtrlQ
);
674 addShortcutKeypress(kbCtrlR
);
675 addShortcutKeypress(kbCtrlS
);
676 addShortcutKeypress(kbCtrlT
);
677 addShortcutKeypress(kbCtrlU
);
678 addShortcutKeypress(kbCtrlV
);
679 addShortcutKeypress(kbCtrlW
);
680 addShortcutKeypress(kbCtrlX
);
681 addShortcutKeypress(kbCtrlY
);
682 addShortcutKeypress(kbCtrlZ
);
683 addShortcutKeypress(kbF1
);
684 addShortcutKeypress(kbF2
);
685 addShortcutKeypress(kbF3
);
686 addShortcutKeypress(kbF4
);
687 addShortcutKeypress(kbF5
);
688 addShortcutKeypress(kbF6
);
689 addShortcutKeypress(kbF7
);
690 addShortcutKeypress(kbF8
);
691 addShortcutKeypress(kbF9
);
692 addShortcutKeypress(kbF10
);
693 addShortcutKeypress(kbF11
);
694 addShortcutKeypress(kbF12
);
695 addShortcutKeypress(kbAltA
);
696 addShortcutKeypress(kbAltB
);
697 addShortcutKeypress(kbAltC
);
698 addShortcutKeypress(kbAltD
);
699 addShortcutKeypress(kbAltE
);
700 addShortcutKeypress(kbAltF
);
701 addShortcutKeypress(kbAltG
);
702 addShortcutKeypress(kbAltH
);
703 addShortcutKeypress(kbAltU
);
704 addShortcutKeypress(kbAltJ
);
705 addShortcutKeypress(kbAltK
);
706 addShortcutKeypress(kbAltL
);
707 addShortcutKeypress(kbAltM
);
708 addShortcutKeypress(kbAltN
);
709 addShortcutKeypress(kbAltO
);
710 addShortcutKeypress(kbAltP
);
711 addShortcutKeypress(kbAltQ
);
712 addShortcutKeypress(kbAltR
);
713 addShortcutKeypress(kbAltS
);
714 addShortcutKeypress(kbAltT
);
715 addShortcutKeypress(kbAltU
);
716 addShortcutKeypress(kbAltV
);
717 addShortcutKeypress(kbAltW
);
718 addShortcutKeypress(kbAltX
);
719 addShortcutKeypress(kbAltY
);
720 addShortcutKeypress(kbAltZ
);
724 * Convert a string array to a whitespace-separated string.
726 * @param array the string array
727 * @return a single string
729 private String
stringArrayToString(final String
[] array
) {
730 StringBuilder sb
= new StringBuilder(array
[0].length());
731 for (int i
= 0; i
< array
.length
; i
++) {
733 if (i
< array
.length
- 1) {
737 return sb
.toString();
743 * @param command the command line to execute
745 private void spawnShell(final String
[] command
) {
748 System.err.printf("spawnShell(): '%s'\n",
749 stringArrayToString(command));
752 vScroller
= new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
756 ECMA48
.DeviceType deviceType
= ECMA48
.DeviceType
.XTERM
;
759 ProcessBuilder pb
= new ProcessBuilder(command
);
760 Map
<String
, String
> env
= pb
.environment();
761 env
.put("TERM", ECMA48
.deviceTypeTerm(deviceType
));
762 env
.put("LANG", ECMA48
.deviceTypeLang(deviceType
, "en"));
763 env
.put("COLUMNS", "80");
764 env
.put("LINES", "24");
765 pb
.redirectErrorStream(true);
767 emulator
= new ECMA48(deviceType
, shell
.getInputStream(),
768 shell
.getOutputStream(), this);
769 } catch (IOException e
) {
770 messageBox(i18n
.getString("errorLaunchingShellTitle"),
771 MessageFormat
.format(i18n
.getString("errorLaunchingShellText"),
775 // Setup the scroll bars
776 onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
, getWidth(),
779 // Claim the keystrokes the emulator will need.
783 newStatusBar(i18n
.getString("statusBarRunning"));
785 // Pass the correct text cell width/height to the emulator
786 emulator
.setTextWidth(getScreen().getTextWidth());
787 emulator
.setTextHeight(getScreen().getTextHeight());
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() {
830 getApplication().postEvent(new TMenuEvent(TMenu
.MID_REPAINT
));
834 * Copy out variables from the emulator that TTerminal has to expose on
837 private void readEmulatorState() {
838 // Synchronize against the emulator so we don't stomp on its reader
840 synchronized (emulator
) {
841 setHiddenMouse(emulator
.hasHiddenMousePointer());
843 setCursorX(emulator
.getCursorX() + 1);
844 setCursorY(emulator
.getCursorY() + 1
845 + (getHeight() - 2 - emulator
.getHeight())
846 - getVerticalValue());
847 setCursorVisible(emulator
.isCursorVisible());
848 if (getCursorX() > getWidth() - 2) {
849 setCursorVisible(false);
851 if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) {
852 setCursorVisible(false);
854 if (emulator
.getScreenTitle().length() > 0) {
855 // Only update the title if the shell is still alive
857 setTitle(emulator
.getScreenTitle());
861 // Check to see if the shell has died.
862 if (!emulator
.isReading() && (shell
!= null)) {
864 int rc
= shell
.exitValue();
865 // The emulator exited on its own, all is fine
866 setTitle(MessageFormat
.format(i18n
.
867 getString("windowTitleCompleted"), getTitle(), rc
));
870 clearShortcutKeypresses();
871 statusBar
.setText(MessageFormat
.format(i18n
.
872 getString("statusBarCompleted"), rc
));
874 } catch (IllegalThreadStateException e
) {
875 // The emulator thread has exited, but the shell Process
876 // hasn't figured that out yet. Do nothing, we will see
877 // this in a future tick.
879 } else if (emulator
.isReading() && (shell
!= null)) {
880 // The shell might be dead, let's check
882 int rc
= shell
.exitValue();
883 // If we got here, the shell died.
884 setTitle(MessageFormat
.format(i18n
.
885 getString("windowTitleCompleted"), getTitle(), rc
));
888 clearShortcutKeypresses();
889 statusBar
.setText(MessageFormat
.format(i18n
.
890 getString("statusBarCompleted"), rc
));
892 } catch (IllegalThreadStateException e
) {
893 // The shell is still running, do nothing.
897 } // synchronized (emulator)
901 * Check if a mouse press/release/motion event coordinate is over the
904 * @param mouse a mouse-based event
905 * @return whether or not the mouse is on the emulator
907 private boolean mouseOnEmulator(final TMouseEvent mouse
) {
909 if (!emulator
.isReading()) {
913 if ((mouse
.getAbsoluteX() >= getAbsoluteX() + 1)
914 && (mouse
.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
915 && (mouse
.getAbsoluteY() >= getAbsoluteY() + 1)
916 && (mouse
.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
924 * Copy a display buffer.
926 * @param buffer the buffer to copy
927 * @return a deep copy of the buffer's data
929 private List
<DisplayLine
> copyBuffer(final List
<DisplayLine
> buffer
) {
930 ArrayList
<DisplayLine
> result
= new ArrayList
<DisplayLine
>(buffer
.size());
931 for (DisplayLine line
: buffer
) {
932 result
.add(new DisplayLine(line
));
938 * Draw glyphs for a double-width or double-height VT100 cell to two
941 * @param line the line this VT100 cell is in
942 * @param x the X position to draw the left half to
943 * @param y the Y position to draw to
944 * @param cell the cell to draw
946 private void putDoubleWidthCharXY(final DisplayLine line
, final int x
,
947 final int y
, final Cell cell
) {
949 int textWidth
= getScreen().getTextWidth();
950 int textHeight
= getScreen().getTextHeight();
951 boolean cursorBlinkVisible
= true;
953 if (getScreen() instanceof SwingTerminal
) {
954 SwingTerminal terminal
= (SwingTerminal
) getScreen();
955 cursorBlinkVisible
= terminal
.getCursorBlinkVisible();
956 } else if (getScreen() instanceof ECMA48Terminal
) {
957 ECMA48Terminal terminal
= (ECMA48Terminal
) getScreen();
959 if (!terminal
.hasSixel()) {
960 // The backend does not have sixel support, draw this as text
962 putCharXY(x
, y
, cell
);
963 putCharXY(x
+ 1, y
, ' ', cell
);
966 cursorBlinkVisible
= blinkState
;
968 // We don't know how to dray glyphs to this screen, draw them as
969 // text and bail out.
970 putCharXY(x
, y
, cell
);
971 putCharXY(x
+ 1, y
, ' ', cell
);
975 if ((textWidth
!= lastTextWidth
) || (textHeight
!= lastTextHeight
)) {
976 // Screen size has changed, reset the font.
977 setupFont(textHeight
);
978 lastTextWidth
= textWidth
;
979 lastTextHeight
= textHeight
;
981 assert (doubleFont
!= null);
984 if (line
.getDoubleHeight() == 1) {
985 // Double-height top half: don't draw the underline.
986 Cell newCell
= new Cell(cell
);
987 newCell
.setUnderline(false);
988 image
= doubleFont
.getImage(newCell
, textWidth
* 2, textHeight
* 2,
991 image
= doubleFont
.getImage(cell
, textWidth
* 2, textHeight
* 2,
995 // Now that we have the double-wide glyph drawn, copy the right
996 // pieces of it to the cells.
997 Cell left
= new Cell(cell
);
998 Cell right
= new Cell(cell
);
1000 BufferedImage leftImage
= null;
1001 BufferedImage rightImage
= null;
1003 System.err.println("image " + image + " textWidth " + textWidth +
1004 " textHeight " + textHeight);
1007 switch (line
.getDoubleHeight()) {
1009 // Top half double height
1010 leftImage
= image
.getSubimage(0, 0, textWidth
, textHeight
);
1011 rightImage
= image
.getSubimage(textWidth
, 0, textWidth
, textHeight
);
1014 // Bottom half double height
1015 leftImage
= image
.getSubimage(0, textHeight
, textWidth
, textHeight
);
1016 rightImage
= image
.getSubimage(textWidth
, textHeight
,
1017 textWidth
, textHeight
);
1020 // Either single height double-width, or error fallback
1021 BufferedImage wideImage
= new BufferedImage(textWidth
* 2,
1022 textHeight
, BufferedImage
.TYPE_INT_ARGB
);
1023 Graphics2D grWide
= wideImage
.createGraphics();
1024 grWide
.drawImage(image
, 0, 0, wideImage
.getWidth(),
1025 wideImage
.getHeight(), null);
1027 leftImage
= wideImage
.getSubimage(0, 0, textWidth
, textHeight
);
1028 rightImage
= wideImage
.getSubimage(textWidth
, 0, textWidth
,
1032 left
.setImage(leftImage
);
1033 right
.setImage(rightImage
);
1034 // Since we have image data, ditch the character here. Otherwise, a
1035 // drawBoxShadow() over the terminal window will show the characters
1036 // which looks wrong.
1039 putCharXY(x
, y
, left
);
1040 putCharXY(x
+ 1, y
, right
);
1044 * Set up the double-width font.
1046 * @param fontSize the size of font to request for the single-width font.
1047 * The double-width font will be 2x this value.
1049 private void setupFont(final int fontSize
) {
1050 doubleFont
= GlyphMaker
.getInstance(fontSize
* 2);
1052 // Special case: the ECMA48 backend needs to have a timer to drive
1054 if (getScreen() instanceof jexer
.backend
.ECMA48Terminal
) {
1056 // Blink every 500 millis.
1058 getApplication().addTimer(millis
, true,
1061 blinkState
= !blinkState
;
1062 getApplication().doRepaint();
1071 // ------------------------------------------------------------------------
1072 // DisplayListener --------------------------------------------------------
1073 // ------------------------------------------------------------------------
1076 * Called by emulator when fresh data has come in.
1078 public void displayChanged() {
1080 getApplication().postEvent(new TMenuEvent(TMenu
.MID_REPAINT
));
1084 * Function to call to obtain the display width.
1086 * @return the number of columns in the display
1088 public int getDisplayWidth() {
1090 return getWidth() - 2;
1096 * Function to call to obtain the display height.
1098 * @return the number of rows in the display
1100 public int getDisplayHeight() {
1102 return getHeight() - 2;