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