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