Demo8 use terminals now
[fanfix.git] / src / jexer / TTerminalWidget.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 Kevin Lamonte
7 *
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:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
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.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer;
30
31 import java.awt.Font;
32 import java.awt.FontMetrics;
33 import java.awt.Graphics2D;
34 import java.awt.image.BufferedImage;
35
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;
43 import java.util.Map;
44 import java.util.ResourceBundle;
45
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.*;
61
62 /**
63 * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a widget.
64 */
65 public class TTerminalWidget extends TScrollableWidget
66 implements DisplayListener {
67
68 /**
69 * Translated strings.
70 */
71 private static final ResourceBundle i18n = ResourceBundle.getBundle(TTerminalWidget.class.getName());
72
73 // ------------------------------------------------------------------------
74 // Variables --------------------------------------------------------------
75 // ------------------------------------------------------------------------
76
77 /**
78 * The emulator.
79 */
80 private ECMA48 emulator;
81
82 /**
83 * The Process created by the shell spawning constructor.
84 */
85 private Process shell;
86
87 /**
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 .
91 */
92 private boolean ptypipe = false;
93
94 /**
95 * Double-height font.
96 */
97 private GlyphMaker doubleFont;
98
99 /**
100 * Last text width value.
101 */
102 private int lastTextWidth = -1;
103
104 /**
105 * Last text height value.
106 */
107 private int lastTextHeight = -1;
108
109 /**
110 * The blink state, used only by ECMA48 backend and when double-width
111 * chars must be drawn.
112 */
113 private boolean blinkState = true;
114
115 /**
116 * Timer flag, used only by ECMA48 backend and when double-width chars
117 * must be drawn.
118 */
119 private boolean haveTimer = false;
120
121 /**
122 * The last seen scrollback lines.
123 */
124 private List<DisplayLine> scrollback;
125
126 /**
127 * The last seen display lines.
128 */
129 private List<DisplayLine> display;
130
131 /**
132 * If true, the display has changed and needs updating.
133 */
134 private volatile boolean dirty = true;
135
136 /**
137 * Time that the display was last updated.
138 */
139 private long lastUpdateTime = 0;
140
141 /**
142 * If true, hide the mouse after typing a keystroke.
143 */
144 private boolean hideMouseWhenTyping = true;
145
146 /**
147 * If true, the mouse should not be displayed because a keystroke was
148 * typed.
149 */
150 private boolean typingHidMouse = false;
151
152 /**
153 * The return value from the emulator.
154 */
155 private int exitValue = -1;
156
157 /**
158 * Title to expose to a window.
159 */
160 private String title = "";
161
162 // ------------------------------------------------------------------------
163 // Constructors -----------------------------------------------------------
164 // ------------------------------------------------------------------------
165
166 /**
167 * Public constructor spawns a custom command line.
168 *
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
173 */
174 public TTerminalWidget(final TWidget parent, final int x,
175 final int y, final String commandLine) {
176
177 this(parent, x, y, commandLine.split("\\s+"));
178 }
179
180 /**
181 * Public constructor spawns a custom command line.
182 *
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
187 */
188 public TTerminalWidget(final TWidget parent, final int x,
189 final int y, final String [] command) {
190
191 super(parent, x, y, 80, 24);
192
193 String [] fullCommand;
194
195 // Spawn a shell and pass its I/O to the other constructor.
196 if ((System.getProperty("jexer.TTerminal.ptypipe") != null)
197 && (System.getProperty("jexer.TTerminal.ptypipe").
198 equals("true"))
199 ) {
200 ptypipe = true;
201 fullCommand = new String[command.length + 1];
202 fullCommand[0] = "ptypipe";
203 System.arraycopy(command, 0, fullCommand, 1, command.length);
204 } else if (System.getProperty("os.name").startsWith("Windows")) {
205 fullCommand = new String[3];
206 fullCommand[0] = "cmd";
207 fullCommand[1] = "/c";
208 fullCommand[2] = stringArrayToString(command);
209 } else if (System.getProperty("os.name").startsWith("Mac")) {
210 fullCommand = new String[6];
211 fullCommand[0] = "script";
212 fullCommand[1] = "-q";
213 fullCommand[2] = "-F";
214 fullCommand[3] = "/dev/null";
215 fullCommand[4] = "-c";
216 fullCommand[5] = stringArrayToString(command);
217 } else {
218 // Default: behave like Linux
219 fullCommand = new String[5];
220 fullCommand[0] = "script";
221 fullCommand[1] = "-fqe";
222 fullCommand[2] = "/dev/null";
223 fullCommand[3] = "-c";
224 fullCommand[4] = stringArrayToString(command);
225 }
226 spawnShell(fullCommand);
227 }
228
229 /**
230 * Public constructor spawns a shell.
231 *
232 * @param parent parent widget
233 * @param x column relative to parent
234 * @param y row relative to parent
235 */
236 public TTerminalWidget(final TWidget parent, final int x, final int y) {
237
238 super(parent, x, y, 80, 24);
239
240 if (System.getProperty("jexer.TTerminal.shell") != null) {
241 String shell = System.getProperty("jexer.TTerminal.shell");
242 if (shell.trim().startsWith("ptypipe")) {
243 ptypipe = true;
244 }
245 spawnShell(shell.split("\\s+"));
246 return;
247 }
248
249 String cmdShellWindows = "cmd.exe";
250
251 // You cannot run a login shell in a bare Process interactively, due
252 // to libc's behavior of buffering when stdin/stdout aren't a tty.
253 // Use 'script' instead to run a shell in a pty. And because BSD and
254 // GNU differ on the '-f' vs '-F' flags, we need two different
255 // commands. Lovely.
256 String cmdShellGNU = "script -fqe /dev/null";
257 String cmdShellBSD = "script -q -F /dev/null";
258
259 // ptypipe is another solution that permits dynamic window resizing.
260 String cmdShellPtypipe = "ptypipe /bin/bash --login";
261
262 // Spawn a shell and pass its I/O to the other constructor.
263 if ((System.getProperty("jexer.TTerminal.ptypipe") != null)
264 && (System.getProperty("jexer.TTerminal.ptypipe").
265 equals("true"))
266 ) {
267 ptypipe = true;
268 spawnShell(cmdShellPtypipe.split("\\s+"));
269 } else if (System.getProperty("os.name").startsWith("Windows")) {
270 spawnShell(cmdShellWindows.split("\\s+"));
271 } else if (System.getProperty("os.name").startsWith("Mac")) {
272 spawnShell(cmdShellBSD.split("\\s+"));
273 } else if (System.getProperty("os.name").startsWith("Linux")) {
274 spawnShell(cmdShellGNU.split("\\s+"));
275 } else {
276 // When all else fails, assume GNU.
277 spawnShell(cmdShellGNU.split("\\s+"));
278 }
279 }
280
281 // ------------------------------------------------------------------------
282 // TScrollableWidget ------------------------------------------------------
283 // ------------------------------------------------------------------------
284
285 /**
286 * Draw the display buffer.
287 */
288 @Override
289 public void draw() {
290 int width = getDisplayWidth();
291 boolean syncEmulator = false;
292 if ((System.currentTimeMillis() - lastUpdateTime >= 25)
293 && (dirty == true)
294 ) {
295 // Too much time has passed, draw it all.
296 syncEmulator = true;
297 } else if (emulator.isReading() && (dirty == false)) {
298 // Wait until the emulator has brought more data in.
299 syncEmulator = false;
300 } else if (!emulator.isReading() && (dirty == true)) {
301 // The emulator won't receive more data, update the display.
302 syncEmulator = true;
303 }
304
305 if ((syncEmulator == true)
306 || (scrollback == null)
307 || (display == null)
308 ) {
309 // We want to minimize the amount of time we have the emulator
310 // locked. Grab a copy of its display.
311 synchronized (emulator) {
312 // Update the scroll bars
313 reflowData();
314
315 if ((scrollback == null) || emulator.isReading()) {
316 scrollback = copyBuffer(emulator.getScrollbackBuffer());
317 display = copyBuffer(emulator.getDisplayBuffer());
318 }
319 width = emulator.getWidth();
320 }
321 dirty = false;
322 }
323
324 // Draw the box using my superclass
325 super.draw();
326
327 // Put together the visible rows
328 int visibleHeight = getHeight();
329 int visibleBottom = scrollback.size() + display.size()
330 + getVerticalValue();
331 assert (visibleBottom >= 0);
332
333 List<DisplayLine> preceedingBlankLines = new ArrayList<DisplayLine>();
334 int visibleTop = visibleBottom - visibleHeight;
335 if (visibleTop < 0) {
336 for (int i = visibleTop; i < 0; i++) {
337 preceedingBlankLines.add(emulator.getBlankDisplayLine());
338 }
339 visibleTop = 0;
340 }
341 assert (visibleTop >= 0);
342
343 List<DisplayLine> displayLines = new ArrayList<DisplayLine>();
344 displayLines.addAll(scrollback);
345 displayLines.addAll(display);
346
347 List<DisplayLine> visibleLines = new ArrayList<DisplayLine>();
348 visibleLines.addAll(preceedingBlankLines);
349 visibleLines.addAll(displayLines.subList(visibleTop,
350 visibleBottom));
351
352 visibleHeight -= visibleLines.size();
353 assert (visibleHeight >= 0);
354
355 // Now draw the emulator screen
356 int row = 0;
357 for (DisplayLine line: visibleLines) {
358 int widthMax = width;
359 if (line.isDoubleWidth()) {
360 widthMax /= 2;
361 }
362 if (widthMax > getWidth()) {
363 widthMax = getWidth();
364 }
365 for (int i = 0; i < widthMax; i++) {
366 Cell ch = line.charAt(i);
367
368 if (ch.isImage()) {
369 putCharXY(i, row, ch);
370 continue;
371 }
372
373 Cell newCell = new Cell(ch);
374 boolean reverse = line.isReverseColor() ^ ch.isReverse();
375 newCell.setReverse(false);
376 if (reverse) {
377 if (ch.getForeColorRGB() < 0) {
378 newCell.setBackColor(ch.getForeColor());
379 newCell.setBackColorRGB(-1);
380 } else {
381 newCell.setBackColorRGB(ch.getForeColorRGB());
382 }
383 if (ch.getBackColorRGB() < 0) {
384 newCell.setForeColor(ch.getBackColor());
385 newCell.setForeColorRGB(-1);
386 } else {
387 newCell.setForeColorRGB(ch.getBackColorRGB());
388 }
389 }
390 if (line.isDoubleWidth()) {
391 putDoubleWidthCharXY(line, (i * 2), row, newCell);
392 } else {
393 putCharXY(i, row, newCell);
394 }
395 }
396 row++;
397 if (row == getHeight()) {
398 // Don't overwrite the box edge
399 break;
400 }
401 }
402 CellAttributes background = new CellAttributes();
403 // Fill in the blank lines on bottom
404 for (int i = 0; i < visibleHeight; i++) {
405 hLineXY(0, i + row, getWidth(), ' ', background);
406 }
407
408 }
409
410 /**
411 * Handle window close.
412 */
413 @Override
414 public void close() {
415 emulator.close();
416 if (shell != null) {
417 terminateShellChildProcess();
418 shell.destroy();
419 shell = null;
420 }
421 }
422
423 /**
424 * Handle window/screen resize events.
425 *
426 * @param resize resize event
427 */
428 @Override
429 public void onResize(final TResizeEvent resize) {
430
431 // Synchronize against the emulator so we don't stomp on its reader
432 // thread.
433 synchronized (emulator) {
434
435 if (resize.getType() == TResizeEvent.Type.WIDGET) {
436 // Resize the scroll bars
437 reflowData();
438 placeScrollbars();
439
440 // Get out of scrollback
441 setVerticalValue(0);
442
443 if (ptypipe) {
444 emulator.setWidth(getWidth());
445 emulator.setHeight(getHeight());
446
447 emulator.writeRemote("\033[8;" + getHeight() + ";" +
448 getWidth() + "t");
449 }
450
451 // Pass the correct text cell width/height to the emulator
452 if (getScreen() != null) {
453 emulator.setTextWidth(getScreen().getTextWidth());
454 emulator.setTextHeight(getScreen().getTextHeight());
455 }
456 }
457 return;
458
459 } // synchronized (emulator)
460 }
461
462 /**
463 * Resize scrollbars for a new width/height.
464 */
465 @Override
466 public void reflowData() {
467
468 // Synchronize against the emulator so we don't stomp on its reader
469 // thread.
470 synchronized (emulator) {
471
472 // Pull cursor information
473 readEmulatorState();
474
475 // Vertical scrollbar
476 setTopValue(getHeight()
477 - (emulator.getScrollbackBuffer().size()
478 + emulator.getDisplayBuffer().size()));
479 setVerticalBigChange(getHeight());
480
481 } // synchronized (emulator)
482 }
483
484 /**
485 * Handle keystrokes.
486 *
487 * @param keypress keystroke event
488 */
489 @Override
490 public void onKeypress(final TKeypressEvent keypress) {
491 if (hideMouseWhenTyping) {
492 typingHidMouse = true;
493 }
494
495 // Scrollback up/down
496 if (keypress.equals(kbShiftPgUp)
497 || keypress.equals(kbCtrlPgUp)
498 || keypress.equals(kbAltPgUp)
499 ) {
500 bigVerticalDecrement();
501 return;
502 }
503 if (keypress.equals(kbShiftPgDn)
504 || keypress.equals(kbCtrlPgDn)
505 || keypress.equals(kbAltPgDn)
506 ) {
507 bigVerticalIncrement();
508 return;
509 }
510
511 if (emulator.isReading()) {
512 // Get out of scrollback
513 setVerticalValue(0);
514 emulator.addUserEvent(keypress);
515
516 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
517 // this is kBEnter then also send kbCtrlJ.
518 if (keypress.equals(kbEnter)) {
519 if (System.getProperty("os.name").startsWith("Windows")
520 && (System.getProperty("jexer.TTerminal.cmdHack",
521 "true").equals("true"))
522 ) {
523 emulator.addUserEvent(new TKeypressEvent(kbCtrlJ));
524 }
525 }
526
527 readEmulatorState();
528 return;
529 }
530
531 // Process is closed, honor "normal" TUI keystrokes
532 super.onKeypress(keypress);
533 }
534
535 /**
536 * Handle mouse press events.
537 *
538 * @param mouse mouse button press event
539 */
540 @Override
541 public void onMouseDown(final TMouseEvent mouse) {
542 if (hideMouseWhenTyping) {
543 typingHidMouse = false;
544 }
545
546 // If the emulator is tracking mouse buttons, it needs to see wheel
547 // events.
548 if (emulator.getMouseProtocol() == ECMA48.MouseProtocol.OFF) {
549 if (mouse.isMouseWheelUp()) {
550 verticalDecrement();
551 return;
552 }
553 if (mouse.isMouseWheelDown()) {
554 verticalIncrement();
555 return;
556 }
557 }
558 if (mouseOnEmulator(mouse)) {
559 emulator.addUserEvent(mouse);
560 readEmulatorState();
561 return;
562 }
563
564 // Emulator didn't consume it, pass it on
565 super.onMouseDown(mouse);
566 }
567
568 /**
569 * Handle mouse release events.
570 *
571 * @param mouse mouse button release event
572 */
573 @Override
574 public void onMouseUp(final TMouseEvent mouse) {
575 if (hideMouseWhenTyping) {
576 typingHidMouse = false;
577 }
578
579 if (mouseOnEmulator(mouse)) {
580 emulator.addUserEvent(mouse);
581 readEmulatorState();
582 return;
583 }
584
585 // Emulator didn't consume it, pass it on
586 super.onMouseUp(mouse);
587 }
588
589 /**
590 * Handle mouse motion events.
591 *
592 * @param mouse mouse motion event
593 */
594 @Override
595 public void onMouseMotion(final TMouseEvent mouse) {
596 if (hideMouseWhenTyping) {
597 typingHidMouse = false;
598 }
599
600 if (mouseOnEmulator(mouse)) {
601 emulator.addUserEvent(mouse);
602 readEmulatorState();
603 return;
604 }
605
606 // Emulator didn't consume it, pass it on
607 super.onMouseMotion(mouse);
608 }
609
610 // ------------------------------------------------------------------------
611 // TTerminalWidget --------------------------------------------------------
612 // ------------------------------------------------------------------------
613
614 /**
615 * Get the desired window title.
616 *
617 * @return the title
618 */
619 public String getTitle() {
620 return title;
621 }
622
623 /**
624 * Returns true if this window does not want the application-wide mouse
625 * cursor drawn over it.
626 *
627 * @return true if this window does not want the application-wide mouse
628 * cursor drawn over it
629 */
630 public boolean hasHiddenMouse() {
631 return (emulator.hasHiddenMousePointer() || typingHidMouse);
632 }
633
634 /**
635 * See if the terminal is still running.
636 *
637 * @return if true, we are still connected to / reading from the remote
638 * side
639 */
640 public boolean isReading() {
641 return emulator.isReading();
642 }
643
644 /**
645 * Convert a string array to a whitespace-separated string.
646 *
647 * @param array the string array
648 * @return a single string
649 */
650 private String stringArrayToString(final String [] array) {
651 StringBuilder sb = new StringBuilder(array[0].length());
652 for (int i = 0; i < array.length; i++) {
653 sb.append(array[i]);
654 if (i < array.length - 1) {
655 sb.append(' ');
656 }
657 }
658 return sb.toString();
659 }
660
661 /**
662 * Spawn the shell.
663 *
664 * @param command the command line to execute
665 */
666 private void spawnShell(final String [] command) {
667
668 /*
669 System.err.printf("spawnShell(): '%s'\n",
670 stringArrayToString(command));
671 */
672
673 // We will have vScroller for its data fields and mouse event
674 // handling, but do not want to draw it.
675 vScroller = new TVScroller(null, getWidth(), 0, getHeight());
676 vScroller.setVisible(false);
677 setBottomValue(0);
678
679 title = i18n.getString("windowTitle");
680
681 // Assume XTERM
682 ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM;
683
684 try {
685 ProcessBuilder pb = new ProcessBuilder(command);
686 Map<String, String> env = pb.environment();
687 env.put("TERM", ECMA48.deviceTypeTerm(deviceType));
688 env.put("LANG", ECMA48.deviceTypeLang(deviceType, "en"));
689 env.put("COLUMNS", "80");
690 env.put("LINES", "24");
691 pb.redirectErrorStream(true);
692 shell = pb.start();
693 emulator = new ECMA48(deviceType, shell.getInputStream(),
694 shell.getOutputStream(), this);
695 } catch (IOException e) {
696 messageBox(i18n.getString("errorLaunchingShellTitle"),
697 MessageFormat.format(i18n.getString("errorLaunchingShellText"),
698 e.getMessage()));
699 }
700
701 // Setup the scroll bars
702 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
703 getHeight()));
704
705 // Hide mouse when typing option
706 if (System.getProperty("jexer.TTerminal.hideMouseWhenTyping",
707 "true").equals("false")) {
708
709 hideMouseWhenTyping = false;
710 }
711 }
712
713 /**
714 * Terminate the child of the 'script' process used on POSIX. This may
715 * or may not work.
716 */
717 private void terminateShellChildProcess() {
718 int pid = -1;
719 if (shell.getClass().getName().equals("java.lang.UNIXProcess")) {
720 /* get the PID on unix/linux systems */
721 try {
722 Field field = shell.getClass().getDeclaredField("pid");
723 field.setAccessible(true);
724 pid = field.getInt(shell);
725 } catch (Throwable e) {
726 // SQUASH, this didn't work. Just bail out quietly.
727 return;
728 }
729 }
730 if (pid != -1) {
731 // shell.destroy() works successfully at killing this side of
732 // 'script'. But we need to make sure the other side (child
733 // process) is also killed.
734 String [] cmdKillIt = {
735 "pkill", "-P", Integer.toString(pid)
736 };
737 try {
738 Runtime.getRuntime().exec(cmdKillIt);
739 } catch (Throwable e) {
740 // SQUASH, this didn't work. Just bail out quietly.
741 return;
742 }
743 }
744 }
745
746 /**
747 * Hook for subclasses to be notified of the shell termination.
748 */
749 public void onShellExit() {
750 if (getParent() instanceof TTerminalWindow) {
751 ((TTerminalWindow) getParent()).onShellExit();
752 }
753 getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
754 }
755
756 /**
757 * Copy out variables from the emulator that TTerminal has to expose on
758 * screen.
759 */
760 private void readEmulatorState() {
761 // Synchronize against the emulator so we don't stomp on its reader
762 // thread.
763 synchronized (emulator) {
764
765 setCursorX(emulator.getCursorX());
766 setCursorY(emulator.getCursorY()
767 + (getHeight() - emulator.getHeight())
768 - getVerticalValue());
769 setCursorVisible(emulator.isCursorVisible());
770 if (getCursorX() > getWidth()) {
771 setCursorVisible(false);
772 }
773 if ((getCursorY() >= getHeight()) || (getCursorY() < 0)) {
774 setCursorVisible(false);
775 }
776 if (emulator.getScreenTitle().length() > 0) {
777 // Only update the title if the shell is still alive
778 if (shell != null) {
779 title = emulator.getScreenTitle();
780 }
781 }
782
783 // Check to see if the shell has died.
784 if (!emulator.isReading() && (shell != null)) {
785 try {
786 int rc = shell.exitValue();
787 // The emulator exited on its own, all is fine
788 title = MessageFormat.format(i18n.
789 getString("windowTitleCompleted"), title, rc);
790 exitValue = rc;
791 shell = null;
792 emulator.close();
793 onShellExit();
794 } catch (IllegalThreadStateException e) {
795 // The emulator thread has exited, but the shell Process
796 // hasn't figured that out yet. Do nothing, we will see
797 // this in a future tick.
798 }
799 } else if (emulator.isReading() && (shell != null)) {
800 // The shell might be dead, let's check
801 try {
802 int rc = shell.exitValue();
803 // If we got here, the shell died.
804 title = MessageFormat.format(i18n.
805 getString("windowTitleCompleted"), title, rc);
806 exitValue = rc;
807 shell = null;
808 emulator.close();
809 onShellExit();
810 } catch (IllegalThreadStateException e) {
811 // The shell is still running, do nothing.
812 }
813 }
814
815 } // synchronized (emulator)
816 }
817
818 /**
819 * Check if a mouse press/release/motion event coordinate is over the
820 * emulator.
821 *
822 * @param mouse a mouse-based event
823 * @return whether or not the mouse is on the emulator
824 */
825 private boolean mouseOnEmulator(final TMouseEvent mouse) {
826
827 if (!emulator.isReading()) {
828 return false;
829 }
830
831 if ((mouse.getX() >= 0)
832 && (mouse.getX() < getWidth() - 1)
833 && (mouse.getY() >= 0)
834 && (mouse.getY() < getHeight())
835 ) {
836 return true;
837 }
838 return false;
839 }
840
841 /**
842 * Copy a display buffer.
843 *
844 * @param buffer the buffer to copy
845 * @return a deep copy of the buffer's data
846 */
847 private List<DisplayLine> copyBuffer(final List<DisplayLine> buffer) {
848 ArrayList<DisplayLine> result = new ArrayList<DisplayLine>(buffer.size());
849 for (DisplayLine line: buffer) {
850 result.add(new DisplayLine(line));
851 }
852 return result;
853 }
854
855 /**
856 * Draw glyphs for a double-width or double-height VT100 cell to two
857 * screen cells.
858 *
859 * @param line the line this VT100 cell is in
860 * @param x the X position to draw the left half to
861 * @param y the Y position to draw to
862 * @param cell the cell to draw
863 */
864 private void putDoubleWidthCharXY(final DisplayLine line, final int x,
865 final int y, final Cell cell) {
866
867 int textWidth = getScreen().getTextWidth();
868 int textHeight = getScreen().getTextHeight();
869 boolean cursorBlinkVisible = true;
870
871 if (getScreen() instanceof SwingTerminal) {
872 SwingTerminal terminal = (SwingTerminal) getScreen();
873 cursorBlinkVisible = terminal.getCursorBlinkVisible();
874 } else if (getScreen() instanceof ECMA48Terminal) {
875 ECMA48Terminal terminal = (ECMA48Terminal) getScreen();
876
877 if (!terminal.hasSixel()) {
878 // The backend does not have sixel support, draw this as text
879 // and bail out.
880 putCharXY(x, y, cell);
881 putCharXY(x + 1, y, ' ', cell);
882 return;
883 }
884 cursorBlinkVisible = blinkState;
885 } else {
886 // We don't know how to dray glyphs to this screen, draw them as
887 // text and bail out.
888 putCharXY(x, y, cell);
889 putCharXY(x + 1, y, ' ', cell);
890 return;
891 }
892
893 if ((textWidth != lastTextWidth) || (textHeight != lastTextHeight)) {
894 // Screen size has changed, reset the font.
895 setupFont(textHeight);
896 lastTextWidth = textWidth;
897 lastTextHeight = textHeight;
898 }
899 assert (doubleFont != null);
900
901 BufferedImage image;
902 if (line.getDoubleHeight() == 1) {
903 // Double-height top half: don't draw the underline.
904 Cell newCell = new Cell(cell);
905 newCell.setUnderline(false);
906 image = doubleFont.getImage(newCell, textWidth * 2, textHeight * 2,
907 cursorBlinkVisible);
908 } else {
909 image = doubleFont.getImage(cell, textWidth * 2, textHeight * 2,
910 cursorBlinkVisible);
911 }
912
913 // Now that we have the double-wide glyph drawn, copy the right
914 // pieces of it to the cells.
915 Cell left = new Cell(cell);
916 Cell right = new Cell(cell);
917 right.setChar(' ');
918 BufferedImage leftImage = null;
919 BufferedImage rightImage = null;
920 /*
921 System.err.println("image " + image + " textWidth " + textWidth +
922 " textHeight " + textHeight);
923 */
924
925 switch (line.getDoubleHeight()) {
926 case 1:
927 // Top half double height
928 leftImage = image.getSubimage(0, 0, textWidth, textHeight);
929 rightImage = image.getSubimage(textWidth, 0, textWidth, textHeight);
930 break;
931 case 2:
932 // Bottom half double height
933 leftImage = image.getSubimage(0, textHeight, textWidth, textHeight);
934 rightImage = image.getSubimage(textWidth, textHeight,
935 textWidth, textHeight);
936 break;
937 default:
938 // Either single height double-width, or error fallback
939 BufferedImage wideImage = new BufferedImage(textWidth * 2,
940 textHeight, BufferedImage.TYPE_INT_ARGB);
941 Graphics2D grWide = wideImage.createGraphics();
942 grWide.drawImage(image, 0, 0, wideImage.getWidth(),
943 wideImage.getHeight(), null);
944 grWide.dispose();
945 leftImage = wideImage.getSubimage(0, 0, textWidth, textHeight);
946 rightImage = wideImage.getSubimage(textWidth, 0, textWidth,
947 textHeight);
948 break;
949 }
950 left.setImage(leftImage);
951 right.setImage(rightImage);
952 // Since we have image data, ditch the character here. Otherwise, a
953 // drawBoxShadow() over the terminal window will show the characters
954 // which looks wrong.
955 left.setChar(' ');
956 right.setChar(' ');
957 putCharXY(x, y, left);
958 putCharXY(x + 1, y, right);
959 }
960
961 /**
962 * Set up the double-width font.
963 *
964 * @param fontSize the size of font to request for the single-width font.
965 * The double-width font will be 2x this value.
966 */
967 private void setupFont(final int fontSize) {
968 doubleFont = GlyphMaker.getInstance(fontSize * 2);
969
970 // Special case: the ECMA48 backend needs to have a timer to drive
971 // its blink state.
972 if (getScreen() instanceof jexer.backend.ECMA48Terminal) {
973 if (!haveTimer) {
974 // Blink every 500 millis.
975 long millis = 500;
976 getApplication().addTimer(millis, true,
977 new TAction() {
978 public void DO() {
979 blinkState = !blinkState;
980 getApplication().doRepaint();
981 }
982 }
983 );
984 haveTimer = true;
985 }
986 }
987 }
988
989 // ------------------------------------------------------------------------
990 // DisplayListener --------------------------------------------------------
991 // ------------------------------------------------------------------------
992
993 /**
994 * Called by emulator when fresh data has come in.
995 */
996 public void displayChanged() {
997 dirty = true;
998 getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
999 }
1000
1001 /**
1002 * Function to call to obtain the display width.
1003 *
1004 * @return the number of columns in the display
1005 */
1006 public int getDisplayWidth() {
1007 if (ptypipe) {
1008 return getWidth();
1009 }
1010 return 80;
1011 }
1012
1013 /**
1014 * Function to call to obtain the display height.
1015 *
1016 * @return the number of rows in the display
1017 */
1018 public int getDisplayHeight() {
1019 if (ptypipe) {
1020 return getHeight();
1021 }
1022 return 24;
1023 }
1024
1025 }