Jexer image protocol
[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 * TTerminalWidget 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 visible display.
123 */
124 private List<DisplayLine> display;
125
126 /**
127 * If true, the display has changed and needs updating.
128 */
129 private volatile boolean dirty = true;
130
131 /**
132 * Time that the display was last updated.
133 */
134 private long lastUpdateTime = 0;
135
136 /**
137 * If true, hide the mouse after typing a keystroke.
138 */
139 private boolean hideMouseWhenTyping = true;
140
141 /**
142 * If true, the mouse should not be displayed because a keystroke was
143 * typed.
144 */
145 private boolean typingHidMouse = false;
146
147 /**
148 * The return value from the emulator.
149 */
150 private int exitValue = -1;
151
152 /**
153 * Title to expose to a window.
154 */
155 private String title = "";
156
157 /**
158 * Action to perform when the terminal exits.
159 */
160 private TAction closeAction = null;
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, final int y,
175 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, final int y,
189 final String [] command) {
190
191 this(parent, x, y, command, null);
192 }
193
194 /**
195 * Public constructor spawns a custom command line.
196 *
197 * @param parent parent widget
198 * @param x column relative to parent
199 * @param y row relative to parent
200 * @param command the command line to execute
201 * @param closeAction action to perform when the shell sxits
202 */
203 public TTerminalWidget(final TWidget parent, final int x, final int y,
204 final String [] command, final TAction closeAction) {
205
206 this(parent, x, y, 80, 24, command, closeAction);
207 }
208
209 /**
210 * Public constructor spawns a custom command line.
211 *
212 * @param parent parent widget
213 * @param x column relative to parent
214 * @param y row relative to parent
215 * @param width width of widget
216 * @param height height of widget
217 * @param command the command line to execute
218 * @param closeAction action to perform when the shell sxits
219 */
220 public TTerminalWidget(final TWidget parent, final int x, final int y,
221 final int width, final int height, final String [] command,
222 final TAction closeAction) {
223
224 super(parent, x, y, width, height);
225
226 this.closeAction = closeAction;
227
228 String [] fullCommand;
229
230 // Spawn a shell and pass its I/O to the other constructor.
231 if ((System.getProperty("jexer.TTerminal.ptypipe") != null)
232 && (System.getProperty("jexer.TTerminal.ptypipe").
233 equals("true"))
234 ) {
235 ptypipe = true;
236 fullCommand = new String[command.length + 1];
237 fullCommand[0] = "ptypipe";
238 System.arraycopy(command, 0, fullCommand, 1, command.length);
239 } else if (System.getProperty("os.name").startsWith("Windows")) {
240 fullCommand = new String[3];
241 fullCommand[0] = "cmd";
242 fullCommand[1] = "/c";
243 fullCommand[2] = stringArrayToString(command);
244 } else if (System.getProperty("os.name").startsWith("Mac")) {
245 fullCommand = new String[6];
246 fullCommand[0] = "script";
247 fullCommand[1] = "-q";
248 fullCommand[2] = "-F";
249 fullCommand[3] = "/dev/null";
250 fullCommand[4] = "-c";
251 fullCommand[5] = stringArrayToString(command);
252 } else {
253 // Default: behave like Linux
254 fullCommand = new String[5];
255 fullCommand[0] = "script";
256 fullCommand[1] = "-fqe";
257 fullCommand[2] = "/dev/null";
258 fullCommand[3] = "-c";
259 fullCommand[4] = stringArrayToString(command);
260 }
261 spawnShell(fullCommand);
262 }
263
264 /**
265 * Public constructor spawns a shell.
266 *
267 * @param parent parent widget
268 * @param x column relative to parent
269 * @param y row relative to parent
270 */
271 public TTerminalWidget(final TWidget parent, final int x, final int y) {
272 this(parent, x, y, (TAction) null);
273 }
274
275 /**
276 * Public constructor spawns a shell.
277 *
278 * @param parent parent widget
279 * @param x column relative to parent
280 * @param y row relative to parent
281 * @param closeAction action to perform when the shell sxits
282 */
283 public TTerminalWidget(final TWidget parent, final int x, final int y,
284 final TAction closeAction) {
285
286 this(parent, x, y, 80, 24, closeAction);
287 }
288
289 /**
290 * Public constructor spawns a shell.
291 *
292 * @param parent parent widget
293 * @param x column relative to parent
294 * @param y row relative to parent
295 * @param width width of widget
296 * @param height height of widget
297 * @param closeAction action to perform when the shell sxits
298 */
299 public TTerminalWidget(final TWidget parent, final int x, final int y,
300 final int width, final int height, final TAction closeAction) {
301
302 super(parent, x, y, width, height);
303
304 this.closeAction = closeAction;
305
306 if (System.getProperty("jexer.TTerminal.shell") != null) {
307 String shell = System.getProperty("jexer.TTerminal.shell");
308 if (shell.trim().startsWith("ptypipe")) {
309 ptypipe = true;
310 }
311 spawnShell(shell.split("\\s+"));
312 return;
313 }
314
315 String cmdShellWindows = "cmd.exe";
316
317 // You cannot run a login shell in a bare Process interactively, due
318 // to libc's behavior of buffering when stdin/stdout aren't a tty.
319 // Use 'script' instead to run a shell in a pty. And because BSD and
320 // GNU differ on the '-f' vs '-F' flags, we need two different
321 // commands. Lovely.
322 String cmdShellGNU = "script -fqe /dev/null";
323 String cmdShellBSD = "script -q -F /dev/null";
324
325 // ptypipe is another solution that permits dynamic window resizing.
326 String cmdShellPtypipe = "ptypipe /bin/bash --login";
327
328 // Spawn a shell and pass its I/O to the other constructor.
329 if ((System.getProperty("jexer.TTerminal.ptypipe") != null)
330 && (System.getProperty("jexer.TTerminal.ptypipe").
331 equals("true"))
332 ) {
333 ptypipe = true;
334 spawnShell(cmdShellPtypipe.split("\\s+"));
335 } else if (System.getProperty("os.name").startsWith("Windows")) {
336 spawnShell(cmdShellWindows.split("\\s+"));
337 } else if (System.getProperty("os.name").startsWith("Mac")) {
338 spawnShell(cmdShellBSD.split("\\s+"));
339 } else if (System.getProperty("os.name").startsWith("Linux")) {
340 spawnShell(cmdShellGNU.split("\\s+"));
341 } else {
342 // When all else fails, assume GNU.
343 spawnShell(cmdShellGNU.split("\\s+"));
344 }
345 }
346
347 // ------------------------------------------------------------------------
348 // Event handlers ---------------------------------------------------------
349 // ------------------------------------------------------------------------
350
351 /**
352 * Handle window/screen resize events.
353 *
354 * @param resize resize event
355 */
356 @Override
357 public void onResize(final TResizeEvent resize) {
358 // Let TWidget set my size.
359 super.onResize(resize);
360
361 if (emulator == null) {
362 return;
363 }
364
365 // Synchronize against the emulator so we don't stomp on its reader
366 // thread.
367 synchronized (emulator) {
368
369 if (resize.getType() == TResizeEvent.Type.WIDGET) {
370 // Resize the scroll bars
371 reflowData();
372 placeScrollbars();
373
374 // Get out of scrollback
375 setVerticalValue(0);
376
377 if (ptypipe) {
378 emulator.setWidth(getWidth());
379 emulator.setHeight(getHeight());
380
381 emulator.writeRemote("\033[8;" + getHeight() + ";" +
382 getWidth() + "t");
383 }
384
385 // Pass the correct text cell width/height to the emulator
386 if (getScreen() != null) {
387 emulator.setTextWidth(getScreen().getTextWidth());
388 emulator.setTextHeight(getScreen().getTextHeight());
389 }
390 }
391 return;
392
393 } // synchronized (emulator)
394 }
395
396 /**
397 * Handle keystrokes.
398 *
399 * @param keypress keystroke event
400 */
401 @Override
402 public void onKeypress(final TKeypressEvent keypress) {
403 if (hideMouseWhenTyping) {
404 typingHidMouse = true;
405 }
406
407 // Scrollback up/down
408 if (keypress.equals(kbShiftPgUp)
409 || keypress.equals(kbCtrlPgUp)
410 || keypress.equals(kbAltPgUp)
411 ) {
412 bigVerticalDecrement();
413 dirty = true;
414 return;
415 }
416 if (keypress.equals(kbShiftPgDn)
417 || keypress.equals(kbCtrlPgDn)
418 || keypress.equals(kbAltPgDn)
419 ) {
420 bigVerticalIncrement();
421 dirty = true;
422 return;
423 }
424
425 if ((emulator != null) && (emulator.isReading())) {
426 // Get out of scrollback
427 setVerticalValue(0);
428 emulator.addUserEvent(keypress);
429
430 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
431 // this is kBEnter then also send kbCtrlJ.
432 if (keypress.equals(kbEnter)) {
433 if (System.getProperty("os.name").startsWith("Windows")
434 && (System.getProperty("jexer.TTerminal.cmdHack",
435 "true").equals("true"))
436 ) {
437 emulator.addUserEvent(new TKeypressEvent(kbCtrlJ));
438 }
439 }
440
441 readEmulatorState();
442 return;
443 }
444
445 // Process is closed, honor "normal" TUI keystrokes
446 super.onKeypress(keypress);
447 }
448
449 /**
450 * Handle mouse press events.
451 *
452 * @param mouse mouse button press event
453 */
454 @Override
455 public void onMouseDown(final TMouseEvent mouse) {
456 if (hideMouseWhenTyping) {
457 typingHidMouse = false;
458 }
459
460 if (emulator != null) {
461 // If the emulator is tracking mouse buttons, it needs to see
462 // wheel events.
463 if (emulator.getMouseProtocol() == ECMA48.MouseProtocol.OFF) {
464 if (mouse.isMouseWheelUp()) {
465 verticalDecrement();
466 dirty = true;
467 return;
468 }
469 if (mouse.isMouseWheelDown()) {
470 verticalIncrement();
471 dirty = true;
472 return;
473 }
474 }
475 if (mouseOnEmulator(mouse)) {
476 emulator.addUserEvent(mouse);
477 readEmulatorState();
478 return;
479 }
480 }
481
482 // Emulator didn't consume it, pass it on
483 super.onMouseDown(mouse);
484 }
485
486 /**
487 * Handle mouse release events.
488 *
489 * @param mouse mouse button release event
490 */
491 @Override
492 public void onMouseUp(final TMouseEvent mouse) {
493 if (hideMouseWhenTyping) {
494 typingHidMouse = false;
495 }
496
497 if ((emulator != null) && (mouseOnEmulator(mouse))) {
498 emulator.addUserEvent(mouse);
499 readEmulatorState();
500 return;
501 }
502
503 // Emulator didn't consume it, pass it on
504 super.onMouseUp(mouse);
505 }
506
507 /**
508 * Handle mouse motion events.
509 *
510 * @param mouse mouse motion event
511 */
512 @Override
513 public void onMouseMotion(final TMouseEvent mouse) {
514 if (hideMouseWhenTyping) {
515 typingHidMouse = false;
516 }
517
518 if ((emulator != null) && (mouseOnEmulator(mouse))) {
519 emulator.addUserEvent(mouse);
520 readEmulatorState();
521 return;
522 }
523
524 // Emulator didn't consume it, pass it on
525 super.onMouseMotion(mouse);
526 }
527
528 // ------------------------------------------------------------------------
529 // TScrollableWidget ------------------------------------------------------
530 // ------------------------------------------------------------------------
531
532 /**
533 * Draw the display buffer.
534 */
535 @Override
536 public void draw() {
537 if (emulator == null) {
538 return;
539 }
540
541 int width = getDisplayWidth();
542
543 boolean syncEmulator = false;
544 if (System.currentTimeMillis() - lastUpdateTime >= 50) {
545 // Too much time has passed, draw it all.
546 syncEmulator = true;
547 } else if (emulator.isReading() && (dirty == false)) {
548 // Wait until the emulator has brought more data in.
549 syncEmulator = false;
550 } else if (!emulator.isReading() && (dirty == true)) {
551 // The emulator won't receive more data, update the display.
552 syncEmulator = true;
553 }
554
555 if ((syncEmulator == true)
556 || (display == null)
557 ) {
558 // We want to minimize the amount of time we have the emulator
559 // locked. Grab a copy of its display.
560 synchronized (emulator) {
561 // Update the scroll bars
562 reflowData();
563
564 if (!isDrawable()) {
565 // We lost the connection, onShellExit() called an action
566 // that ultimately removed this widget from the UI
567 // hierarchy, so no one cares if we update the display.
568 // Bail out.
569 return;
570 }
571
572 if ((display == null) || emulator.isReading()) {
573 display = emulator.getVisibleDisplay(getHeight(),
574 -getVerticalValue());
575 assert (display.size() == getHeight());
576 }
577 width = emulator.getWidth();
578 }
579 dirty = false;
580 }
581
582 // Now draw the emulator screen
583 int row = 0;
584 for (DisplayLine line: display) {
585 int widthMax = width;
586 if (line.isDoubleWidth()) {
587 widthMax /= 2;
588 }
589 if (widthMax > getWidth()) {
590 widthMax = getWidth();
591 }
592 for (int i = 0; i < widthMax; i++) {
593 Cell ch = line.charAt(i);
594
595 if (ch.isImage()) {
596 putCharXY(i, row, ch);
597 continue;
598 }
599
600 Cell newCell = new Cell(ch);
601 boolean reverse = line.isReverseColor() ^ ch.isReverse();
602 newCell.setReverse(false);
603 if (reverse) {
604 if (ch.getForeColorRGB() < 0) {
605 newCell.setBackColor(ch.getForeColor());
606 newCell.setBackColorRGB(-1);
607 } else {
608 newCell.setBackColorRGB(ch.getForeColorRGB());
609 }
610 if (ch.getBackColorRGB() < 0) {
611 newCell.setForeColor(ch.getBackColor());
612 newCell.setForeColorRGB(-1);
613 } else {
614 newCell.setForeColorRGB(ch.getBackColorRGB());
615 }
616 }
617 if (line.isDoubleWidth()) {
618 putDoubleWidthCharXY(line, (i * 2), row, newCell);
619 } else {
620 putCharXY(i, row, newCell);
621 }
622 }
623 row++;
624 }
625 }
626
627 /**
628 * Set current value of the vertical scroll.
629 *
630 * @param value the new scroll value
631 */
632 @Override
633 public void setVerticalValue(final int value) {
634 super.setVerticalValue(value);
635 dirty = true;
636 }
637
638 /**
639 * Perform a small step change up.
640 */
641 @Override
642 public void verticalDecrement() {
643 super.verticalDecrement();
644 dirty = true;
645 }
646
647 /**
648 * Perform a small step change down.
649 */
650 @Override
651 public void verticalIncrement() {
652 super.verticalIncrement();
653 dirty = true;
654 }
655
656 /**
657 * Perform a big step change up.
658 */
659 public void bigVerticalDecrement() {
660 super.bigVerticalDecrement();
661 dirty = true;
662 }
663
664 /**
665 * Perform a big step change down.
666 */
667 public void bigVerticalIncrement() {
668 super.bigVerticalIncrement();
669 dirty = true;
670 }
671
672 /**
673 * Go to the top edge of the vertical scroller.
674 */
675 public void toTop() {
676 super.toTop();
677 dirty = true;
678 }
679
680 /**
681 * Go to the bottom edge of the vertical scroller.
682 */
683 public void toBottom() {
684 super.toBottom();
685 dirty = true;
686 }
687
688 /**
689 * Handle widget close.
690 */
691 @Override
692 public void close() {
693 if (emulator != null) {
694 emulator.close();
695 }
696 if (shell != null) {
697 terminateShellChildProcess();
698 shell.destroy();
699 shell = null;
700 }
701 }
702
703 /**
704 * Resize scrollbars for a new width/height.
705 */
706 @Override
707 public void reflowData() {
708 if (emulator == null) {
709 return;
710 }
711
712 // Synchronize against the emulator so we don't stomp on its reader
713 // thread.
714 synchronized (emulator) {
715
716 // Pull cursor information
717 readEmulatorState();
718
719 // Vertical scrollbar
720 setTopValue(getHeight()
721 - (emulator.getScrollbackBuffer().size()
722 + emulator.getDisplayBuffer().size()));
723 setVerticalBigChange(getHeight());
724
725 } // synchronized (emulator)
726 }
727
728 // ------------------------------------------------------------------------
729 // TTerminalWidget --------------------------------------------------------
730 // ------------------------------------------------------------------------
731
732 /**
733 * Get the desired window title.
734 *
735 * @return the title
736 */
737 public String getTitle() {
738 return title;
739 }
740
741 /**
742 * Returns true if this widget does not want the application-wide mouse
743 * cursor drawn over it.
744 *
745 * @return true if this widget does not want the application-wide mouse
746 * cursor drawn over it
747 */
748 public boolean hasHiddenMouse() {
749 if (emulator == null) {
750 return false;
751 }
752 return (emulator.hasHiddenMousePointer() || typingHidMouse);
753 }
754
755 /**
756 * See if the terminal is still running.
757 *
758 * @return if true, we are still connected to / reading from the remote
759 * side
760 */
761 public boolean isReading() {
762 if (emulator == null) {
763 return false;
764 }
765 return emulator.isReading();
766 }
767
768 /**
769 * Convert a string array to a whitespace-separated string.
770 *
771 * @param array the string array
772 * @return a single string
773 */
774 private String stringArrayToString(final String [] array) {
775 StringBuilder sb = new StringBuilder(array[0].length());
776 for (int i = 0; i < array.length; i++) {
777 sb.append(array[i]);
778 if (i < array.length - 1) {
779 sb.append(' ');
780 }
781 }
782 return sb.toString();
783 }
784
785 /**
786 * Spawn the shell.
787 *
788 * @param command the command line to execute
789 */
790 private void spawnShell(final String [] command) {
791
792 /*
793 System.err.printf("spawnShell(): '%s'\n",
794 stringArrayToString(command));
795 */
796
797 // We will have vScroller for its data fields and mouse event
798 // handling, but do not want to draw it.
799 vScroller = new TVScroller(null, getWidth(), 0, getHeight());
800 vScroller.setVisible(false);
801 setBottomValue(0);
802
803 title = i18n.getString("windowTitle");
804
805 // Assume XTERM
806 ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM;
807
808 try {
809 ProcessBuilder pb = new ProcessBuilder(command);
810 Map<String, String> env = pb.environment();
811 env.put("TERM", ECMA48.deviceTypeTerm(deviceType));
812 env.put("LANG", ECMA48.deviceTypeLang(deviceType, "en"));
813 env.put("COLUMNS", "80");
814 env.put("LINES", "24");
815 pb.redirectErrorStream(true);
816 shell = pb.start();
817 emulator = new ECMA48(deviceType, shell.getInputStream(),
818 shell.getOutputStream(), this);
819 } catch (IOException e) {
820 messageBox(i18n.getString("errorLaunchingShellTitle"),
821 MessageFormat.format(i18n.getString("errorLaunchingShellText"),
822 e.getMessage()));
823 }
824
825 // Setup the scroll bars
826 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
827 getHeight()));
828
829 // Hide mouse when typing option
830 if (System.getProperty("jexer.TTerminal.hideMouseWhenTyping",
831 "true").equals("false")) {
832
833 hideMouseWhenTyping = false;
834 }
835 }
836
837 /**
838 * Terminate the child of the 'script' process used on POSIX. This may
839 * or may not work.
840 */
841 private void terminateShellChildProcess() {
842 int pid = -1;
843 if (shell.getClass().getName().equals("java.lang.UNIXProcess")) {
844 /* get the PID on unix/linux systems */
845 try {
846 Field field = shell.getClass().getDeclaredField("pid");
847 field.setAccessible(true);
848 pid = field.getInt(shell);
849 } catch (Throwable e) {
850 // SQUASH, this didn't work. Just bail out quietly.
851 return;
852 }
853 }
854 if (pid != -1) {
855 // shell.destroy() works successfully at killing this side of
856 // 'script'. But we need to make sure the other side (child
857 // process) is also killed.
858 String [] cmdKillIt = {
859 "pkill", "-P", Integer.toString(pid)
860 };
861 try {
862 Runtime.getRuntime().exec(cmdKillIt);
863 } catch (Throwable e) {
864 // SQUASH, this didn't work. Just bail out quietly.
865 return;
866 }
867 }
868 }
869
870 /**
871 * Hook for subclasses to be notified of the shell termination.
872 */
873 public void onShellExit() {
874 TApplication app = getApplication();
875 if (app != null) {
876 if (closeAction != null) {
877 // We have to put this action inside invokeLater() because it
878 // could be executed during draw() when syncing with ECMA48.
879 app.invokeLater(new Runnable() {
880 public void run() {
881 closeAction.DO(TTerminalWidget.this);
882 }
883 });
884 }
885 if (getApplication() != null) {
886 getApplication().postEvent(new TMenuEvent(
887 TMenu.MID_REPAINT));
888 }
889 }
890 }
891
892 /**
893 * Copy out variables from the emulator that TTerminal has to expose on
894 * screen.
895 */
896 private void readEmulatorState() {
897 if (emulator == null) {
898 return;
899 }
900
901 // Synchronize against the emulator so we don't stomp on its reader
902 // thread.
903 synchronized (emulator) {
904
905 setCursorX(emulator.getCursorX());
906 setCursorY(emulator.getCursorY()
907 + (getHeight() - emulator.getHeight())
908 - getVerticalValue());
909 setCursorVisible(emulator.isCursorVisible());
910 if (getCursorX() > getWidth()) {
911 setCursorVisible(false);
912 }
913 if ((getCursorY() >= getHeight()) || (getCursorY() < 0)) {
914 setCursorVisible(false);
915 }
916 if (emulator.getScreenTitle().length() > 0) {
917 // Only update the title if the shell is still alive
918 if (shell != null) {
919 title = emulator.getScreenTitle();
920 }
921 }
922
923 // Check to see if the shell has died.
924 if (!emulator.isReading() && (shell != null)) {
925 try {
926 int rc = shell.exitValue();
927 // The emulator exited on its own, all is fine
928 title = MessageFormat.format(i18n.
929 getString("windowTitleCompleted"), title, rc);
930 exitValue = rc;
931 shell = null;
932 emulator.close();
933 onShellExit();
934 } catch (IllegalThreadStateException e) {
935 // The emulator thread has exited, but the shell Process
936 // hasn't figured that out yet. Do nothing, we will see
937 // this in a future tick.
938 }
939 } else if (emulator.isReading() && (shell != null)) {
940 // The shell might be dead, let's check
941 try {
942 int rc = shell.exitValue();
943 // If we got here, the shell died.
944 title = MessageFormat.format(i18n.
945 getString("windowTitleCompleted"), title, rc);
946 exitValue = rc;
947 shell = null;
948 emulator.close();
949 onShellExit();
950 } catch (IllegalThreadStateException e) {
951 // The shell is still running, do nothing.
952 }
953 }
954
955 } // synchronized (emulator)
956 }
957
958 /**
959 * Check if a mouse press/release/motion event coordinate is over the
960 * emulator.
961 *
962 * @param mouse a mouse-based event
963 * @return whether or not the mouse is on the emulator
964 */
965 private boolean mouseOnEmulator(final TMouseEvent mouse) {
966 if (emulator == null) {
967 return false;
968 }
969
970 if (!emulator.isReading()) {
971 return false;
972 }
973
974 if ((mouse.getX() >= 0)
975 && (mouse.getX() < getWidth() - 1)
976 && (mouse.getY() >= 0)
977 && (mouse.getY() < getHeight())
978 ) {
979 return true;
980 }
981 return false;
982 }
983
984 /**
985 * Draw glyphs for a double-width or double-height VT100 cell to two
986 * screen cells.
987 *
988 * @param line the line this VT100 cell is in
989 * @param x the X position to draw the left half to
990 * @param y the Y position to draw to
991 * @param cell the cell to draw
992 */
993 private void putDoubleWidthCharXY(final DisplayLine line, final int x,
994 final int y, final Cell cell) {
995
996 int textWidth = getScreen().getTextWidth();
997 int textHeight = getScreen().getTextHeight();
998 boolean cursorBlinkVisible = true;
999
1000 if (getScreen() instanceof SwingTerminal) {
1001 SwingTerminal terminal = (SwingTerminal) getScreen();
1002 cursorBlinkVisible = terminal.getCursorBlinkVisible();
1003 } else if (getScreen() instanceof ECMA48Terminal) {
1004 ECMA48Terminal terminal = (ECMA48Terminal) getScreen();
1005
1006 if (!terminal.hasSixel()) {
1007 // The backend does not have sixel support, draw this as text
1008 // and bail out.
1009 putCharXY(x, y, cell);
1010 putCharXY(x + 1, y, ' ', cell);
1011 return;
1012 }
1013 cursorBlinkVisible = blinkState;
1014 } else {
1015 // We don't know how to dray glyphs to this screen, draw them as
1016 // text and bail out.
1017 putCharXY(x, y, cell);
1018 putCharXY(x + 1, y, ' ', cell);
1019 return;
1020 }
1021
1022 if ((textWidth != lastTextWidth) || (textHeight != lastTextHeight)) {
1023 // Screen size has changed, reset the font.
1024 setupFont(textHeight);
1025 lastTextWidth = textWidth;
1026 lastTextHeight = textHeight;
1027 }
1028 assert (doubleFont != null);
1029
1030 BufferedImage image;
1031 if (line.getDoubleHeight() == 1) {
1032 // Double-height top half: don't draw the underline.
1033 Cell newCell = new Cell(cell);
1034 newCell.setUnderline(false);
1035 image = doubleFont.getImage(newCell, textWidth * 2, textHeight * 2,
1036 cursorBlinkVisible);
1037 } else {
1038 image = doubleFont.getImage(cell, textWidth * 2, textHeight * 2,
1039 cursorBlinkVisible);
1040 }
1041
1042 // Now that we have the double-wide glyph drawn, copy the right
1043 // pieces of it to the cells.
1044 Cell left = new Cell(cell);
1045 Cell right = new Cell(cell);
1046 right.setChar(' ');
1047 BufferedImage leftImage = null;
1048 BufferedImage rightImage = null;
1049 /*
1050 System.err.println("image " + image + " textWidth " + textWidth +
1051 " textHeight " + textHeight);
1052 */
1053
1054 switch (line.getDoubleHeight()) {
1055 case 1:
1056 // Top half double height
1057 leftImage = image.getSubimage(0, 0, textWidth, textHeight);
1058 rightImage = image.getSubimage(textWidth, 0, textWidth, textHeight);
1059 break;
1060 case 2:
1061 // Bottom half double height
1062 leftImage = image.getSubimage(0, textHeight, textWidth, textHeight);
1063 rightImage = image.getSubimage(textWidth, textHeight,
1064 textWidth, textHeight);
1065 break;
1066 default:
1067 // Either single height double-width, or error fallback
1068 BufferedImage wideImage = new BufferedImage(textWidth * 2,
1069 textHeight, BufferedImage.TYPE_INT_ARGB);
1070 Graphics2D grWide = wideImage.createGraphics();
1071 grWide.drawImage(image, 0, 0, wideImage.getWidth(),
1072 wideImage.getHeight(), null);
1073 grWide.dispose();
1074 leftImage = wideImage.getSubimage(0, 0, textWidth, textHeight);
1075 rightImage = wideImage.getSubimage(textWidth, 0, textWidth,
1076 textHeight);
1077 break;
1078 }
1079 left.setImage(leftImage);
1080 right.setImage(rightImage);
1081 // Since we have image data, ditch the character here. Otherwise, a
1082 // drawBoxShadow() over the terminal window will show the characters
1083 // which looks wrong.
1084 left.setChar(' ');
1085 right.setChar(' ');
1086 putCharXY(x, y, left);
1087 putCharXY(x + 1, y, right);
1088 }
1089
1090 /**
1091 * Set up the double-width font.
1092 *
1093 * @param fontSize the size of font to request for the single-width font.
1094 * The double-width font will be 2x this value.
1095 */
1096 private void setupFont(final int fontSize) {
1097 doubleFont = GlyphMaker.getInstance(fontSize * 2);
1098
1099 // Special case: the ECMA48 backend needs to have a timer to drive
1100 // its blink state.
1101 if (getScreen() instanceof jexer.backend.ECMA48Terminal) {
1102 if (!haveTimer) {
1103 // Blink every 500 millis.
1104 long millis = 500;
1105 getApplication().addTimer(millis, true,
1106 new TAction() {
1107 public void DO() {
1108 blinkState = !blinkState;
1109 getApplication().doRepaint();
1110 }
1111 }
1112 );
1113 haveTimer = true;
1114 }
1115 }
1116 }
1117
1118 // ------------------------------------------------------------------------
1119 // DisplayListener --------------------------------------------------------
1120 // ------------------------------------------------------------------------
1121
1122 /**
1123 * Called by emulator when fresh data has come in.
1124 */
1125 public void displayChanged() {
1126 if (emulator != null) {
1127 // Force sync here: EMCA48.run() thread might be setting
1128 // dirty=true while TTerminalWdiget.draw() is setting
1129 // dirty=false. If these writes start interleaving, the display
1130 // stops getting updated.
1131 synchronized (emulator) {
1132 dirty = true;
1133 }
1134 } else {
1135 dirty = true;
1136 }
1137 getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
1138 }
1139
1140 /**
1141 * Function to call to obtain the display width.
1142 *
1143 * @return the number of columns in the display
1144 */
1145 public int getDisplayWidth() {
1146 if (ptypipe) {
1147 return getWidth();
1148 }
1149 return 80;
1150 }
1151
1152 /**
1153 * Function to call to obtain the display height.
1154 *
1155 * @return the number of rows in the display
1156 */
1157 public int getDisplayHeight() {
1158 if (ptypipe) {
1159 return getHeight();
1160 }
1161 return 24;
1162 }
1163
1164 }