fix scrollbar
[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 // Synchronize against the emulator so we don't stomp on its reader
362 // thread.
363 synchronized (emulator) {
364
365 if (resize.getType() == TResizeEvent.Type.WIDGET) {
366 // Resize the scroll bars
367 reflowData();
368 placeScrollbars();
369
370 // Get out of scrollback
371 setVerticalValue(0);
372
373 if (ptypipe) {
374 emulator.setWidth(getWidth());
375 emulator.setHeight(getHeight());
376
377 emulator.writeRemote("\033[8;" + getHeight() + ";" +
378 getWidth() + "t");
379 }
380
381 // Pass the correct text cell width/height to the emulator
382 if (getScreen() != null) {
383 emulator.setTextWidth(getScreen().getTextWidth());
384 emulator.setTextHeight(getScreen().getTextHeight());
385 }
386 }
387 return;
388
389 } // synchronized (emulator)
390 }
391
392 /**
393 * Handle keystrokes.
394 *
395 * @param keypress keystroke event
396 */
397 @Override
398 public void onKeypress(final TKeypressEvent keypress) {
399 if (hideMouseWhenTyping) {
400 typingHidMouse = true;
401 }
402
403 // Scrollback up/down
404 if (keypress.equals(kbShiftPgUp)
405 || keypress.equals(kbCtrlPgUp)
406 || keypress.equals(kbAltPgUp)
407 ) {
408 bigVerticalDecrement();
409 dirty = true;
410 return;
411 }
412 if (keypress.equals(kbShiftPgDn)
413 || keypress.equals(kbCtrlPgDn)
414 || keypress.equals(kbAltPgDn)
415 ) {
416 bigVerticalIncrement();
417 dirty = true;
418 return;
419 }
420
421 if (emulator.isReading()) {
422 // Get out of scrollback
423 setVerticalValue(0);
424 emulator.addUserEvent(keypress);
425
426 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
427 // this is kBEnter then also send kbCtrlJ.
428 if (keypress.equals(kbEnter)) {
429 if (System.getProperty("os.name").startsWith("Windows")
430 && (System.getProperty("jexer.TTerminal.cmdHack",
431 "true").equals("true"))
432 ) {
433 emulator.addUserEvent(new TKeypressEvent(kbCtrlJ));
434 }
435 }
436
437 readEmulatorState();
438 return;
439 }
440
441 // Process is closed, honor "normal" TUI keystrokes
442 super.onKeypress(keypress);
443 }
444
445 /**
446 * Handle mouse press events.
447 *
448 * @param mouse mouse button press event
449 */
450 @Override
451 public void onMouseDown(final TMouseEvent mouse) {
452 if (hideMouseWhenTyping) {
453 typingHidMouse = false;
454 }
455
456 // If the emulator is tracking mouse buttons, it needs to see wheel
457 // events.
458 if (emulator.getMouseProtocol() == ECMA48.MouseProtocol.OFF) {
459 if (mouse.isMouseWheelUp()) {
460 verticalDecrement();
461 dirty = true;
462 return;
463 }
464 if (mouse.isMouseWheelDown()) {
465 verticalIncrement();
466 dirty = true;
467 return;
468 }
469 }
470 if (mouseOnEmulator(mouse)) {
471 emulator.addUserEvent(mouse);
472 readEmulatorState();
473 return;
474 }
475
476 // Emulator didn't consume it, pass it on
477 super.onMouseDown(mouse);
478 }
479
480 /**
481 * Handle mouse release events.
482 *
483 * @param mouse mouse button release event
484 */
485 @Override
486 public void onMouseUp(final TMouseEvent mouse) {
487 if (hideMouseWhenTyping) {
488 typingHidMouse = false;
489 }
490
491 if (mouseOnEmulator(mouse)) {
492 emulator.addUserEvent(mouse);
493 readEmulatorState();
494 return;
495 }
496
497 // Emulator didn't consume it, pass it on
498 super.onMouseUp(mouse);
499 }
500
501 /**
502 * Handle mouse motion events.
503 *
504 * @param mouse mouse motion event
505 */
506 @Override
507 public void onMouseMotion(final TMouseEvent mouse) {
508 if (hideMouseWhenTyping) {
509 typingHidMouse = false;
510 }
511
512 if (mouseOnEmulator(mouse)) {
513 emulator.addUserEvent(mouse);
514 readEmulatorState();
515 return;
516 }
517
518 // Emulator didn't consume it, pass it on
519 super.onMouseMotion(mouse);
520 }
521
522 // ------------------------------------------------------------------------
523 // TScrollableWidget ------------------------------------------------------
524 // ------------------------------------------------------------------------
525
526 /**
527 * Draw the display buffer.
528 */
529 @Override
530 public void draw() {
531 int width = getDisplayWidth();
532
533 boolean syncEmulator = false;
534 if ((System.currentTimeMillis() - lastUpdateTime >= 20)
535 && (dirty == true)
536 ) {
537 // Too much time has passed, draw it all.
538 syncEmulator = true;
539 } else if (emulator.isReading() && (dirty == false)) {
540 // Wait until the emulator has brought more data in.
541 syncEmulator = false;
542 } else if (!emulator.isReading() && (dirty == true)) {
543 // The emulator won't receive more data, update the display.
544 syncEmulator = true;
545 }
546
547 if ((syncEmulator == true)
548 || (display == null)
549 ) {
550 // We want to minimize the amount of time we have the emulator
551 // locked. Grab a copy of its display.
552 synchronized (emulator) {
553 // Update the scroll bars
554 reflowData();
555
556 if (!isDrawable()) {
557 // We lost the connection, onShellExit() called an action
558 // that ultimately removed this widget from the UI
559 // hierarchy, so no one cares if we update the display.
560 // Bail out.
561 return;
562 }
563
564 if ((display == null) || emulator.isReading()) {
565 display = emulator.getVisibleDisplay(getHeight(),
566 -getVerticalValue());
567 assert (display.size() == getHeight());
568 }
569 width = emulator.getWidth();
570 }
571 dirty = false;
572 }
573
574 // Now draw the emulator screen
575 int row = 0;
576 for (DisplayLine line: display) {
577 int widthMax = width;
578 if (line.isDoubleWidth()) {
579 widthMax /= 2;
580 }
581 if (widthMax > getWidth()) {
582 widthMax = getWidth();
583 }
584 for (int i = 0; i < widthMax; i++) {
585 Cell ch = line.charAt(i);
586
587 if (ch.isImage()) {
588 putCharXY(i, row, ch);
589 continue;
590 }
591
592 Cell newCell = new Cell(ch);
593 boolean reverse = line.isReverseColor() ^ ch.isReverse();
594 newCell.setReverse(false);
595 if (reverse) {
596 if (ch.getForeColorRGB() < 0) {
597 newCell.setBackColor(ch.getForeColor());
598 newCell.setBackColorRGB(-1);
599 } else {
600 newCell.setBackColorRGB(ch.getForeColorRGB());
601 }
602 if (ch.getBackColorRGB() < 0) {
603 newCell.setForeColor(ch.getBackColor());
604 newCell.setForeColorRGB(-1);
605 } else {
606 newCell.setForeColorRGB(ch.getBackColorRGB());
607 }
608 }
609 if (line.isDoubleWidth()) {
610 putDoubleWidthCharXY(line, (i * 2), row, newCell);
611 } else {
612 putCharXY(i, row, newCell);
613 }
614 }
615 row++;
616 }
617 }
618
619 /**
620 * Set current value of the vertical scroll.
621 *
622 * @param value the new scroll value
623 */
624 @Override
625 public void setVerticalValue(final int value) {
626 super.setVerticalValue(value);
627 dirty = true;
628 }
629
630 /**
631 * Perform a small step change up.
632 */
633 @Override
634 public void verticalDecrement() {
635 super.verticalDecrement();
636 dirty = true;
637 }
638
639 /**
640 * Perform a small step change down.
641 */
642 @Override
643 public void verticalIncrement() {
644 super.verticalIncrement();
645 dirty = true;
646 }
647
648 /**
649 * Perform a big step change up.
650 */
651 public void bigVerticalDecrement() {
652 super.bigVerticalDecrement();
653 dirty = true;
654 }
655
656 /**
657 * Perform a big step change down.
658 */
659 public void bigVerticalIncrement() {
660 super.bigVerticalIncrement();
661 dirty = true;
662 }
663
664 /**
665 * Go to the top edge of the vertical scroller.
666 */
667 public void toTop() {
668 super.toTop();
669 dirty = true;
670 }
671
672 /**
673 * Go to the bottom edge of the vertical scroller.
674 */
675 public void toBottom() {
676 super.toBottom();
677 dirty = true;
678 }
679
680 /**
681 * Handle widget close.
682 */
683 @Override
684 public void close() {
685 emulator.close();
686 if (shell != null) {
687 terminateShellChildProcess();
688 shell.destroy();
689 shell = null;
690 }
691 }
692
693 /**
694 * Resize scrollbars for a new width/height.
695 */
696 @Override
697 public void reflowData() {
698
699 // Synchronize against the emulator so we don't stomp on its reader
700 // thread.
701 synchronized (emulator) {
702
703 // Pull cursor information
704 readEmulatorState();
705
706 // Vertical scrollbar
707 setTopValue(getHeight()
708 - (emulator.getScrollbackBuffer().size()
709 + emulator.getDisplayBuffer().size()));
710 setVerticalBigChange(getHeight());
711
712 } // synchronized (emulator)
713 }
714
715 // ------------------------------------------------------------------------
716 // TTerminalWidget --------------------------------------------------------
717 // ------------------------------------------------------------------------
718
719 /**
720 * Get the desired window title.
721 *
722 * @return the title
723 */
724 public String getTitle() {
725 return title;
726 }
727
728 /**
729 * Returns true if this widget does not want the application-wide mouse
730 * cursor drawn over it.
731 *
732 * @return true if this widget does not want the application-wide mouse
733 * cursor drawn over it
734 */
735 public boolean hasHiddenMouse() {
736 return (emulator.hasHiddenMousePointer() || typingHidMouse);
737 }
738
739 /**
740 * See if the terminal is still running.
741 *
742 * @return if true, we are still connected to / reading from the remote
743 * side
744 */
745 public boolean isReading() {
746 return emulator.isReading();
747 }
748
749 /**
750 * Convert a string array to a whitespace-separated string.
751 *
752 * @param array the string array
753 * @return a single string
754 */
755 private String stringArrayToString(final String [] array) {
756 StringBuilder sb = new StringBuilder(array[0].length());
757 for (int i = 0; i < array.length; i++) {
758 sb.append(array[i]);
759 if (i < array.length - 1) {
760 sb.append(' ');
761 }
762 }
763 return sb.toString();
764 }
765
766 /**
767 * Spawn the shell.
768 *
769 * @param command the command line to execute
770 */
771 private void spawnShell(final String [] command) {
772
773 /*
774 System.err.printf("spawnShell(): '%s'\n",
775 stringArrayToString(command));
776 */
777
778 // We will have vScroller for its data fields and mouse event
779 // handling, but do not want to draw it.
780 vScroller = new TVScroller(null, getWidth(), 0, getHeight());
781 vScroller.setVisible(false);
782 setBottomValue(0);
783
784 title = i18n.getString("windowTitle");
785
786 // Assume XTERM
787 ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM;
788
789 try {
790 ProcessBuilder pb = new ProcessBuilder(command);
791 Map<String, String> env = pb.environment();
792 env.put("TERM", ECMA48.deviceTypeTerm(deviceType));
793 env.put("LANG", ECMA48.deviceTypeLang(deviceType, "en"));
794 env.put("COLUMNS", "80");
795 env.put("LINES", "24");
796 pb.redirectErrorStream(true);
797 shell = pb.start();
798 emulator = new ECMA48(deviceType, shell.getInputStream(),
799 shell.getOutputStream(), this);
800 } catch (IOException e) {
801 messageBox(i18n.getString("errorLaunchingShellTitle"),
802 MessageFormat.format(i18n.getString("errorLaunchingShellText"),
803 e.getMessage()));
804 }
805
806 // Setup the scroll bars
807 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
808 getHeight()));
809
810 // Hide mouse when typing option
811 if (System.getProperty("jexer.TTerminal.hideMouseWhenTyping",
812 "true").equals("false")) {
813
814 hideMouseWhenTyping = false;
815 }
816 }
817
818 /**
819 * Terminate the child of the 'script' process used on POSIX. This may
820 * or may not work.
821 */
822 private void terminateShellChildProcess() {
823 int pid = -1;
824 if (shell.getClass().getName().equals("java.lang.UNIXProcess")) {
825 /* get the PID on unix/linux systems */
826 try {
827 Field field = shell.getClass().getDeclaredField("pid");
828 field.setAccessible(true);
829 pid = field.getInt(shell);
830 } catch (Throwable e) {
831 // SQUASH, this didn't work. Just bail out quietly.
832 return;
833 }
834 }
835 if (pid != -1) {
836 // shell.destroy() works successfully at killing this side of
837 // 'script'. But we need to make sure the other side (child
838 // process) is also killed.
839 String [] cmdKillIt = {
840 "pkill", "-P", Integer.toString(pid)
841 };
842 try {
843 Runtime.getRuntime().exec(cmdKillIt);
844 } catch (Throwable e) {
845 // SQUASH, this didn't work. Just bail out quietly.
846 return;
847 }
848 }
849 }
850
851 /**
852 * Hook for subclasses to be notified of the shell termination.
853 */
854 public void onShellExit() {
855 TApplication app = getApplication();
856 if (app != null) {
857 if (closeAction != null) {
858 // We have to put this action inside invokeLater() because it
859 // could be executed during draw() when syncing with ECMA48.
860 app.invokeLater(new Runnable() {
861 public void run() {
862 closeAction.DO(TTerminalWidget.this);
863 }
864 });
865 }
866 if (getApplication() != null) {
867 getApplication().postEvent(new TMenuEvent(
868 TMenu.MID_REPAINT));
869 }
870 }
871 }
872
873 /**
874 * Copy out variables from the emulator that TTerminal has to expose on
875 * screen.
876 */
877 private void readEmulatorState() {
878 // Synchronize against the emulator so we don't stomp on its reader
879 // thread.
880 synchronized (emulator) {
881
882 setCursorX(emulator.getCursorX());
883 setCursorY(emulator.getCursorY()
884 + (getHeight() - emulator.getHeight())
885 - getVerticalValue());
886 setCursorVisible(emulator.isCursorVisible());
887 if (getCursorX() > getWidth()) {
888 setCursorVisible(false);
889 }
890 if ((getCursorY() >= getHeight()) || (getCursorY() < 0)) {
891 setCursorVisible(false);
892 }
893 if (emulator.getScreenTitle().length() > 0) {
894 // Only update the title if the shell is still alive
895 if (shell != null) {
896 title = emulator.getScreenTitle();
897 }
898 }
899
900 // Check to see if the shell has died.
901 if (!emulator.isReading() && (shell != null)) {
902 try {
903 int rc = shell.exitValue();
904 // The emulator exited on its own, all is fine
905 title = MessageFormat.format(i18n.
906 getString("windowTitleCompleted"), title, rc);
907 exitValue = rc;
908 shell = null;
909 emulator.close();
910 onShellExit();
911 } catch (IllegalThreadStateException e) {
912 // The emulator thread has exited, but the shell Process
913 // hasn't figured that out yet. Do nothing, we will see
914 // this in a future tick.
915 }
916 } else if (emulator.isReading() && (shell != null)) {
917 // The shell might be dead, let's check
918 try {
919 int rc = shell.exitValue();
920 // If we got here, the shell died.
921 title = MessageFormat.format(i18n.
922 getString("windowTitleCompleted"), title, rc);
923 exitValue = rc;
924 shell = null;
925 emulator.close();
926 onShellExit();
927 } catch (IllegalThreadStateException e) {
928 // The shell is still running, do nothing.
929 }
930 }
931
932 } // synchronized (emulator)
933 }
934
935 /**
936 * Check if a mouse press/release/motion event coordinate is over the
937 * emulator.
938 *
939 * @param mouse a mouse-based event
940 * @return whether or not the mouse is on the emulator
941 */
942 private boolean mouseOnEmulator(final TMouseEvent mouse) {
943
944 if (!emulator.isReading()) {
945 return false;
946 }
947
948 if ((mouse.getX() >= 0)
949 && (mouse.getX() < getWidth() - 1)
950 && (mouse.getY() >= 0)
951 && (mouse.getY() < getHeight())
952 ) {
953 return true;
954 }
955 return false;
956 }
957
958 /**
959 * Draw glyphs for a double-width or double-height VT100 cell to two
960 * screen cells.
961 *
962 * @param line the line this VT100 cell is in
963 * @param x the X position to draw the left half to
964 * @param y the Y position to draw to
965 * @param cell the cell to draw
966 */
967 private void putDoubleWidthCharXY(final DisplayLine line, final int x,
968 final int y, final Cell cell) {
969
970 int textWidth = getScreen().getTextWidth();
971 int textHeight = getScreen().getTextHeight();
972 boolean cursorBlinkVisible = true;
973
974 if (getScreen() instanceof SwingTerminal) {
975 SwingTerminal terminal = (SwingTerminal) getScreen();
976 cursorBlinkVisible = terminal.getCursorBlinkVisible();
977 } else if (getScreen() instanceof ECMA48Terminal) {
978 ECMA48Terminal terminal = (ECMA48Terminal) getScreen();
979
980 if (!terminal.hasSixel()) {
981 // The backend does not have sixel support, draw this as text
982 // and bail out.
983 putCharXY(x, y, cell);
984 putCharXY(x + 1, y, ' ', cell);
985 return;
986 }
987 cursorBlinkVisible = blinkState;
988 } else {
989 // We don't know how to dray glyphs to this screen, draw them as
990 // text and bail out.
991 putCharXY(x, y, cell);
992 putCharXY(x + 1, y, ' ', cell);
993 return;
994 }
995
996 if ((textWidth != lastTextWidth) || (textHeight != lastTextHeight)) {
997 // Screen size has changed, reset the font.
998 setupFont(textHeight);
999 lastTextWidth = textWidth;
1000 lastTextHeight = textHeight;
1001 }
1002 assert (doubleFont != null);
1003
1004 BufferedImage image;
1005 if (line.getDoubleHeight() == 1) {
1006 // Double-height top half: don't draw the underline.
1007 Cell newCell = new Cell(cell);
1008 newCell.setUnderline(false);
1009 image = doubleFont.getImage(newCell, textWidth * 2, textHeight * 2,
1010 cursorBlinkVisible);
1011 } else {
1012 image = doubleFont.getImage(cell, textWidth * 2, textHeight * 2,
1013 cursorBlinkVisible);
1014 }
1015
1016 // Now that we have the double-wide glyph drawn, copy the right
1017 // pieces of it to the cells.
1018 Cell left = new Cell(cell);
1019 Cell right = new Cell(cell);
1020 right.setChar(' ');
1021 BufferedImage leftImage = null;
1022 BufferedImage rightImage = null;
1023 /*
1024 System.err.println("image " + image + " textWidth " + textWidth +
1025 " textHeight " + textHeight);
1026 */
1027
1028 switch (line.getDoubleHeight()) {
1029 case 1:
1030 // Top half double height
1031 leftImage = image.getSubimage(0, 0, textWidth, textHeight);
1032 rightImage = image.getSubimage(textWidth, 0, textWidth, textHeight);
1033 break;
1034 case 2:
1035 // Bottom half double height
1036 leftImage = image.getSubimage(0, textHeight, textWidth, textHeight);
1037 rightImage = image.getSubimage(textWidth, textHeight,
1038 textWidth, textHeight);
1039 break;
1040 default:
1041 // Either single height double-width, or error fallback
1042 BufferedImage wideImage = new BufferedImage(textWidth * 2,
1043 textHeight, BufferedImage.TYPE_INT_ARGB);
1044 Graphics2D grWide = wideImage.createGraphics();
1045 grWide.drawImage(image, 0, 0, wideImage.getWidth(),
1046 wideImage.getHeight(), null);
1047 grWide.dispose();
1048 leftImage = wideImage.getSubimage(0, 0, textWidth, textHeight);
1049 rightImage = wideImage.getSubimage(textWidth, 0, textWidth,
1050 textHeight);
1051 break;
1052 }
1053 left.setImage(leftImage);
1054 right.setImage(rightImage);
1055 // Since we have image data, ditch the character here. Otherwise, a
1056 // drawBoxShadow() over the terminal window will show the characters
1057 // which looks wrong.
1058 left.setChar(' ');
1059 right.setChar(' ');
1060 putCharXY(x, y, left);
1061 putCharXY(x + 1, y, right);
1062 }
1063
1064 /**
1065 * Set up the double-width font.
1066 *
1067 * @param fontSize the size of font to request for the single-width font.
1068 * The double-width font will be 2x this value.
1069 */
1070 private void setupFont(final int fontSize) {
1071 doubleFont = GlyphMaker.getInstance(fontSize * 2);
1072
1073 // Special case: the ECMA48 backend needs to have a timer to drive
1074 // its blink state.
1075 if (getScreen() instanceof jexer.backend.ECMA48Terminal) {
1076 if (!haveTimer) {
1077 // Blink every 500 millis.
1078 long millis = 500;
1079 getApplication().addTimer(millis, true,
1080 new TAction() {
1081 public void DO() {
1082 blinkState = !blinkState;
1083 getApplication().doRepaint();
1084 }
1085 }
1086 );
1087 haveTimer = true;
1088 }
1089 }
1090 }
1091
1092 // ------------------------------------------------------------------------
1093 // DisplayListener --------------------------------------------------------
1094 // ------------------------------------------------------------------------
1095
1096 /**
1097 * Called by emulator when fresh data has come in.
1098 */
1099 public void displayChanged() {
1100 dirty = true;
1101 getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
1102 }
1103
1104 /**
1105 * Function to call to obtain the display width.
1106 *
1107 * @return the number of columns in the display
1108 */
1109 public int getDisplayWidth() {
1110 if (ptypipe) {
1111 return getWidth();
1112 }
1113 return 80;
1114 }
1115
1116 /**
1117 * Function to call to obtain the display height.
1118 *
1119 * @return the number of rows in the display
1120 */
1121 public int getDisplayHeight() {
1122 if (ptypipe) {
1123 return getHeight();
1124 }
1125 return 24;
1126 }
1127
1128 }